Sequential steps, each working on what the last one produced.
The analogy: an assembly line. Each station does one small job to the thing in front of it and passes it along. Nobody decides where the work goes - the order is fixed by you, in code.
Use it when you already know the steps. "Transcribe, then extract the action items, then rewrite them as a checklist" is three prompts in a fixed order, and a single prompt asking for all three at once does each of them worse.
alias ExAgent.Patterns.Chain
{:ok, checklist} =
Chain.run(transcript,
steps: [
Chain.llm(provider, &"Extract the action items:\n\n#{&1}"),
Chain.llm(provider, &"Rewrite these as a markdown checklist:\n\n#{&1}")
]
)Gates between steps
A step is any function from the previous value to {:ok, next},
{:error, reason}, or {:halt, value}. :halt stops the line early and is
not a failure - it is how you decline to spend the rest of the calls:
Chain.run(ticket,
steps: [
Chain.llm(triage, &"Is this a bug report? Answer YES or NO.\n\n#{&1}"),
fn answer ->
if String.starts_with?(answer, "YES"), do: {:ok, ticket}, else: {:halt, :not_a_bug}
end,
Chain.llm(engineer, &"Suggest a fix:\n\n#{&1}")
]
)A gate is also where a human belongs. Return {:halt, :needs_approval}, park
the work, and start a second chain when someone approves it - the pattern needs
nothing special for that.
When not to use it
If the order depends on the input, you want ExAgent.Patterns.Router. If the
steps are unknown until an LLM decides them, you want
ExAgent.Patterns.Subagents. A chain is deliberately dumb: that is what makes
it cheap to debug.
Summary
Types
Where to send a prompt: a provider struct for a stateless call, or a running agent when the step should remember the conversation.
Functions
Builds a step that sends a prompt to target and returns its text.
Runs input through every step in order.
Types
@type chain_opts() :: [{:steps, [step()]}]
@type target() :: struct() | GenServer.server()
Where to send a prompt: a provider struct for a stateless call, or a running agent when the step should remember the conversation.
@type value() :: term()
Functions
Builds a step that sends a prompt to target and returns its text.
target is a provider struct for a stateless call, or a running agent when the
step should remember the conversation. build_prompt turns the previous value
into the prompt.
Chain.llm(provider, fn summary -> "Translate to French:\n\n#{summary}" end)
@spec run(value(), chain_opts()) :: {:ok, value()} | {:halted, value()} | {:error, {non_neg_integer(), term()}}
Runs input through every step in order.
Returns {:ok, value} when every step succeeded, {:halted, value} when a step
stopped the line, and {:error, reason} on the first failure - along with the
index of the step that failed, since "step 3 of 5" is the first thing you want
to know.