Normandy.DSL.Workflow
(normandy v2.2.0)
View Source
DSL for defining multi-agent workflows with a declarative syntax.
Provides macros to compose complex agent interactions in a readable, maintainable way.
Examples
defmodule ResearchWorkflow do
use Normandy.DSL.Workflow
workflow do
# Sequential steps
step :gather_sources do
agent ResearchAgent
input "Find sources about quantum computing"
end
step :analyze_sources do
agent AnalysisAgent
input from: :gather_sources
transform fn %{chat_message: sources} -> "Analyze these: " <> sources end
end
# Parallel execution
parallel :validate do
agent FactCheckAgent, name: :fact_check
agent QualityAgent, name: :quality_check
input from: :analyze_sources
transform fn %{chat_message: analysis} -> analysis end
end
# Conditional execution: the clause's value becomes the step result. A
# parallel step's result maps each agent's name to its {:ok, answer} or
# {:error, exception}.
step :final_review do
agent ReviewAgent
input from: :validate
transform fn %{fact_check: {:ok, facts}, quality_check: {:ok, quality}} ->
"Facts: " <> facts.chat_message <> "\nQuality: " <> quality.chat_message
end
when_result do
{:ok, %{quality: q}} when q > 0.8 -> :approve
{:ok, _} -> :needs_revision
end
end
end
end
# Execute the workflow
{:ok, result} = ResearchWorkflow.execute(
agents: %{
ResearchAgent => research_agent,
AnalysisAgent => analysis_agent
},
initial_input: "quantum computing"
)Workflow Patterns
Sequential Steps
step :step1 do
agent MyAgent
input "some input"
end
step :step2 do
agent AnotherAgent
input from: :step1
endParallel Execution
parallel :check do
agent Agent1, name: :check1
agent Agent2, name: :check2
input "same input for all"
endConditional Execution
when_result works like Normandy.Coordination.Reactive.when_result/3: it runs
after the step, matches {:ok, result}, and the matching clause's value becomes
the step's result (what later steps and from: see). {:ok, value} gives
value, {:error, reason} fails the workflow as {:error, {step_name, reason}},
and any other value is the result as it is. A clause can run another agent.
step :process do
agent MyAgent
input from: :previous
when_result do
{:ok, %{chat_message: "REJECT" <> _}} -> {:error, :rejected}
{:ok, %{confidence: c}} when c < 0.5 -> :low_confidence
{:ok, result} -> {:ok, result}
end
endA blank answer never reaches when_result: the agent's run already failed with
%Normandy.LLMError{type: :invalid_output}, so the workflow returns
{:error, {step_name, %Normandy.LLMError{}}}.
Transforming Input
transform receives the step's input (a literal, a from: result, or the
previous step's result) and returns what the agent gets.
step :summarize do
agent Summarizer
input from: :research
transform fn %{chat_message: text} -> "Summarize: " <> text end
endRace Pattern
race :fastest do
agent FastAgent1, name: :fast1
agent FastAgent2, name: :fast2
input "Who can answer fastest?"
endTelemetry
execute/1 runs under a [:normandy, :workflow, :run] span (:start, :stop,
:exception) with metadata :workflow_name (the module), :orchestrator
(:workflow) and :step_count. Agent runs started by the workflow's steps
nest under it, so a trace shows workflow -> run -> llm_call/tool. The
orchestrators in Normandy.Coordination emit the same span from their own
entry points (see Normandy.Telemetry); Reactive's combinators
are building blocks used inside this span and emit none of their own (a current
limitation).
Features
- Sequential, parallel, and race execution
- Data flow between steps
- Conditional execution
- Result transformation
- Error handling
- Compile-time workflow validation: duplicate step names, steps without an
agent, and
from:references to unknown or later steps raise at compile time
Summary
Functions
Specifies an agent for the current step.
Specifies the input for the current step.
Defines a parallel execution step.
Defines a race execution step.
Defines a sequential step in the workflow.
Transforms the step's input before passing it to the agent. Applies to every
kind of input: a literal, a from: result, or the previous step's result.
Replaces the step's result with the value of the clause matching
{:ok, result}, like Normandy.Coordination.Reactive.when_result/3.
Defines a workflow block.
Functions
Specifies an agent for the current step.
Options
name:- Optional name for this agent in parallel/race steps
Specifies the input for the current step.
Can be:
- A string literal
from: :step_nameto use output from another step- A map
Defines a parallel execution step.
All agents run concurrently with the same input.
Defines a race execution step.
Returns the first successful result.
Defines a sequential step in the workflow.
Options
agent- Agent module or instance to useinput- Input for the agent (string, orfrom: :step_name)transform- Function applied to the step's input before the agent runswhen_result- Clauses on{:ok, result}whose value becomes the step result
Transforms the step's input before passing it to the agent. Applies to every
kind of input: a literal, a from: result, or the previous step's result.
Examples
transform fn prev_output ->
"Process this: #{prev_output}"
end
Replaces the step's result with the value of the clause matching
{:ok, result}, like Normandy.Coordination.Reactive.when_result/3.
{:ok, value} makes value the result, {:error, reason} fails the workflow,
and any other value becomes the result as it is.
Examples
when_result do
{:ok, %{chat_message: "REJECT" <> _}} -> {:error, :rejected}
{:ok, %{confidence: c}} when c < 0.5 -> :low_confidence
{:ok, result} -> {:ok, result}
endA blank answer fails the step before when_result runs (see "Conditional Execution"
in the moduledoc).
Defines a workflow block.