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
end

Parallel Execution

parallel :check do
  agent Agent1, name: :check1
  agent Agent2, name: :check2
  input "same input for all"
end

Conditional 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
end

A 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
end

Race Pattern

race :fastest do
  agent FastAgent1, name: :fast1
  agent FastAgent2, name: :fast2
  input "Who can answer fastest?"
end

Telemetry

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

agent(module, opts \\ [])

(macro)

Specifies an agent for the current step.

Options

  • name: - Optional name for this agent in parallel/race steps

input(value)

(macro)

Specifies the input for the current step.

Can be:

  • A string literal
  • from: :step_name to use output from another step
  • A map

parallel(name, list)

(macro)

Defines a parallel execution step.

All agents run concurrently with the same input.

race(name, list)

(macro)

Defines a race execution step.

Returns the first successful result.

step(name, list)

(macro)

Defines a sequential step in the workflow.

Options

  • agent - Agent module or instance to use
  • input - Input for the agent (string, or from: :step_name)
  • transform - Function applied to the step's input before the agent runs
  • when_result - Clauses on {:ok, result} whose value becomes the step result

transform(fun)

(macro)

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

when_result(list)

(macro)

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}
end

A blank answer fails the step before when_result runs (see "Conditional Execution" in the moduledoc).

workflow(list)

(macro)

Defines a workflow block.