Pipelines API
A pipeline is a multi-step workflow that can be described in code or in JSON. See the pipelines guide for the narrative version; this page is the reference.
Pipeline
Pipeline
Pipeline(
agent_configs: Sequence["AgentConfig"] | None = None,
default_agent_name: str | None = None,
)
Main pipeline class that manages the execution of multiple processes.
The pipeline maintains a list of processes and executes them in sequence, passing results between processes.
Initialize an empty pipeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_configs
|
Sequence['AgentConfig'] | None
|
Optional declarative agent/endpoint configurations. These round-trip through JSON; the agents themselves are built at load time from environment-held API keys. |
None
|
default_agent_name
|
str | None
|
Name of the declared agent used by processes that name none. |
None
|
add_agent_config
Declare an agent/endpoint that processes can reference by name.
run_pipeline
run_pipeline(**kwargs) -> PipelineResults
Execute all processes in the pipeline sequentially.
Results from each process are passed as input to the next process.
Keyword arguments become the inputs section; step results land in
outputs. The returned object still reads like the flat dictionary
it replaced — results["greeting"] resolves by scope order — so
existing calling code is unaffected.
run
run(results: PipelineResults) -> PipelineResults
Execute every process against an existing results object.
run_pipeline is the usual entry point; this one is for a caller
that needs to seed a section other than inputs -- shared prompt
text, for instance -- before the run begins.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
include_tool_plugins: bool = True,
) -> dict[str, Any]
Serialize this pipeline to a JSON-compatible dictionary.
Runtime agents are intentionally omitted. Pass agents back into
from_dict/load_from_json when restoring a runnable pipeline.
from_dict
classmethod
from_dict(
data: Mapping[str, Any],
tool_registry: PipelineToolRegistry | None = None,
default_agent: BaseToolAgent | None = None,
process_agents: (
Mapping[str, BaseToolAgent] | None
) = None,
step_agents: Mapping[str, BaseToolAgent] | None = None,
load_tool_plugins: bool = True,
build_agents: bool = True,
allow_writes: bool = False,
) -> "Pipeline"
Restore a pipeline from a JSON-compatible dictionary.
If load_tool_plugins is true, plugin declarations in the JSON are
imported with Python's import machinery. Only load JSON files that you
trust, or pass a prebuilt tool_registry and disable plugin loading.
If build_agents is true, any agents block in the JSON is used
to construct providers, reading API keys from the environment
variables the config names. Pass build_agents=False to ignore the
block entirely and supply every agent from Python instead.
allow_writes gates sinks that write files or make HTTP requests.
It is false by default: loading a document should not let it reach
outside the process unless the caller says so. Sources that only read
are always permitted.
Agents injected here always win over names declared in the JSON at the
same level of specificity; see :class:PipelineLoadContext.
to_json
to_json(
tool_registry: PipelineToolRegistry | None = None,
include_tool_plugins: bool = True,
indent: int | None = 2,
) -> str
Serialize this pipeline to a JSON string.
from_json
classmethod
from_json(
json_text: str,
tool_registry: PipelineToolRegistry | None = None,
default_agent: BaseToolAgent | None = None,
process_agents: (
Mapping[str, BaseToolAgent] | None
) = None,
step_agents: Mapping[str, BaseToolAgent] | None = None,
load_tool_plugins: bool = True,
build_agents: bool = True,
allow_writes: bool = False,
) -> "Pipeline"
Restore a pipeline from a JSON string.
save_to_json
save_to_json(
filepath: str | PathLike[str],
tool_registry: PipelineToolRegistry | None = None,
include_tool_plugins: bool = True,
indent: int | None = 2,
) -> None
Write this pipeline to a JSON file.
load_from_json
classmethod
load_from_json(
filepath: str | PathLike[str],
tool_registry: PipelineToolRegistry | None = None,
default_agent: BaseToolAgent | None = None,
process_agents: (
Mapping[str, BaseToolAgent] | None
) = None,
step_agents: Mapping[str, BaseToolAgent] | None = None,
load_tool_plugins: bool = True,
build_agents: bool = True,
allow_writes: bool = False,
) -> "Pipeline"
Load a pipeline from a JSON file.
Results
Results are carried in named sections — inputs, outputs, vars — addressed
by path in prompt templates ({outputs/draft}) and by subscript in conditions
(outputs['draft']). A bare name still resolves innermost-first.
PipelineResults
PipelineResults(
inputs: Mapping[str, Any] | None = None,
outputs: Mapping[str, Any] | None = None,
vars: Mapping[str, Any] | None = None,
sections: Mapping[str, Mapping[str, Any]] | None = None,
)
Bases: MutableMapping
A sectioned results mapping that still behaves like the old flat dict.
Reading and writing a bare key works exactly as before, so existing pipelines, prompt templates and calling code need no changes::
results["draft"] # resolves through vars -> outputs -> inputs
results["draft"] = "..." # writes to outputs
The structure is available whenever it is wanted::
results.outputs["draft"]
results["outputs/draft"]
results["outputs/news/draft"]
coerce
classmethod
Return values as a PipelineResults.
A plain mapping is treated as inputs, so a caller that still hands over a flat dictionary keeps working.
copy
Return a copy with independent section dictionaries.
Section dictionaries are copied; the values inside them are not. A body that mutates a nested list or dict still affects the original, which is why bodies should rebind rather than mutate.
section
Return a section by name, optionally creating it.
Adding a section is how a new kind of state joins the namespace — for
example results.section("agent", create=True) for agent internals.
to_dict
Return the sectioned structure as plain dictionaries.
resolve_path
Resolve path and return (found, value).
A path beginning with a section name is read from that section; anything else is treated as a bare name and resolved by scope order. Returning a flag rather than raising lets templates and conditions each decide what an absent value means.
set_path
Write value at path, creating intermediate mappings.
Processes
Every process implements run_process and serializes to JSON. Flow-control
processes hold other processes, so they nest freely.
Process
Process(
process_name: str = "Process",
agent: BaseToolAgent = None,
agent_name: str | None = None,
)
Bases: ABC
Abstract base class representing a process in the pipeline.
A process is a collection of steps that need to be executed in a specific way. The actual execution logic is defined by concrete implementations.
Initialize a new process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
process_name
|
str
|
Name identifier for the process |
'Process'
|
agent
|
BaseToolAgent
|
Default agent to use for steps that don't have their own agent |
None
|
agent_name
|
str | None
|
Optional name of an agent declared in the pipeline's
|
None
|
add_step
add_step(step: ProcessStep) -> 'Process'
Add a new step to the process and return self, for chaining.
add_steps
add_steps(steps: list[ProcessStep]) -> 'Process'
Add new steps to the process and return self, for chaining.
run_process
abstractmethod
run_process(results: PipelineResults) -> PipelineResults
Execute the process steps according to the implementation logic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
results
|
PipelineResults
|
Sectioned results carried through the pipeline |
required |
Returns:
| Name | Type | Description |
|---|---|---|
PipelineResults |
PipelineResults
|
Updated results after process execution |
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this process to a JSON-compatible dictionary.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "Process"
Rebuild this process from JSON, using context for agents/tools.
Flow-control processes call :func:processes_from_config with the same
context (or a nested one) to rebuild their children.
run_child_processes
run_child_processes(
processes: Sequence["Process"], results: PipelineResults
) -> PipelineResults
Run child processes in order, threading the results mapping.
A child with no agent of its own inherits this process's agent, so a loop or branch built in Python needs the agent set only once, at the outermost level that has one.
ProcessStep
ProcessStep(
step_name: str,
system_message: str,
prompt_template: str,
tools: list[FunctionTool] = None,
agent: BaseToolAgent = None,
agent_name: str | None = None,
)
Represents a single step in a process pipeline for LLM tool usage.
Each step contains the necessary configuration for the LLM to perform a specific task, including system message, prompt template, and available tools.
Attributes:
| Name | Type | Description |
|---|---|---|
step_name |
str
|
The name identifier for the step.
Can be used to reference results of previous steps in the prompt
template, for example |
system_message |
str
|
The system message to provide context to the LLM |
prompt_template |
str
|
Template string for generating the actual prompt |
tools |
list[FunctionTool]
|
List of tools available for this step |
agent |
BaseToolAgent
|
The LLM agent responsible for executing this step |
Initialize a new process step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step_name
|
str
|
Unique identifier for the step. Its result is written
to the |
required |
system_message
|
str
|
Context message for the LLM |
required |
prompt_template
|
str
|
Template for generating the actual prompt |
required |
tools
|
list[FunctionTool]
|
Optional list of tools available for this step |
None
|
agent
|
BaseToolAgent
|
Optional specific agent for this step |
None
|
agent_name
|
str | None
|
Optional name of an agent declared in the pipeline's
|
None
|
get_system_message
Return the system message, with any placeholders filled in.
A system message is a prompt as much as prompt_template is, so it
is rendered against the same results. That is what lets a shared
prompt file be addressed as {prompts/reviewer} in either field.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fields
|
'PipelineResults | Mapping[str, Any] | None'
|
A results mapping. Omit it to get the raw template. |
None
|
get_prompt
Generate the actual prompt using the template and provided parameters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fields
|
'PipelineResults | Mapping[str, Any] | None'
|
A results mapping. Passing the mapping itself, rather than
unpacking it, is what lets a template address a section:
|
None
|
**kwargs
|
Any
|
Individual template fields, for callers not using a results mapping. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
The generated prompt |
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this step to a JSON-compatible dictionary.
Agents are runtime objects and are intentionally not serialized.
from_dict
classmethod
from_dict(
data: Mapping[str, Any],
tool_registry: PipelineToolRegistry | None = None,
agent: BaseToolAgent | None = None,
) -> "ProcessStep"
Restore a process step from a JSON-compatible dictionary.
SequentialProcess
SequentialProcess(
process_name: str = "SequentialProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
)
Bases: Process
Concrete implementation of Process that executes steps sequentially.
Each step is executed in order, with results from previous steps available to subsequent steps through the results dictionary.
Initialize a sequential process with optional default agent.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this sequential process to a JSON-compatible dictionary.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "SequentialProcess"
Restore a sequential process from its JSON representation.
run_process
run_process(results: PipelineResults) -> PipelineResults
Execute process steps in sequential order.
For each step: 1. Set up tool registry if tools are available 2. Prepare messages with system message and generated prompt 3. Execute step using appropriate agent 4. Store results in results dictionary
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
results
|
PipelineResults
|
Dictionary containing results from previous processes |
required |
Returns:
| Type | Description |
|---|---|
PipelineResults
|
dict[str, Any]: Updated results dictionary after all steps are executed |
Raises:
| Type | Description |
|---|---|
Exception
|
If no agent is available for a step |
TemplateProcess
TemplateProcess(
template: str,
result_key: str = "text",
section: str = "outputs",
process_name: str = "TemplateProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
)
Bases: Process
Compose a value from existing results, without calling a model.
Joining two results together is string work, not reasoning. Without this
the only way to do it is to ask a model to "return this unchanged", which
costs a request, adds latency, and is not guaranteed to comply.
JSON::
{
"process_type": "template",
"process_name": "assemble",
"template": "# {outputs/title}
{outputs/body}", "result_key": "digest" }
Initialize a template process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
template
|
str
|
Text with |
required |
result_key
|
str
|
Key the rendered text is written to. |
'text'
|
section
|
str
|
Results section written to. Defaults to |
'outputs'
|
process_name
|
str
|
Name identifier for the process. |
'TemplateProcess'
|
agent
|
BaseToolAgent
|
Unused; this process calls no model. Accepted so it can sit anywhere a process can. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Render the template and store the result.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this template process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "TemplateProcess"
Restore a template process from JSON.
Flow control
FlowProcess
FlowProcess(
process_name: str = "Process",
agent: BaseToolAgent = None,
agent_name: str | None = None,
)
Bases: Process
Base class for processes whose body is a list of other processes.
add_step is kept working as a convenience: a step added to a flow
process is appended to a trailing :class:SequentialProcess in its primary
body, created on demand. Without this, add_step on a loop would append
to the unused steps list inherited from Process and silently never
run.
primary_body
primary_body() -> list[Process]
Return the process list that add_step/add_process extend.
child_bodies
child_bodies() -> tuple[list[Process], ...]
Return every child process list, for traversal.
add_process
add_process(process: Process) -> 'FlowProcess'
Append a child process to this process's primary body.
add_processes
add_processes(
processes: Sequence[Process],
) -> "FlowProcess"
Append several child processes to this process's primary body.
add_step
add_step(step: ProcessStep) -> 'FlowProcess'
Append a step to the trailing sequence of this process's body.
add_steps
add_steps(steps: Sequence[ProcessStep]) -> 'FlowProcess'
Append several steps to the trailing sequence of this body.
ConditionalProcess
ConditionalProcess(
condition: Condition | Mapping[str, Any] | str,
process_name: str = "ConditionalProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
then_processes: Sequence[Process] | None = None,
else_processes: Sequence[Process] | None = None,
then_steps: Sequence[ProcessStep] | None = None,
else_steps: Sequence[ProcessStep] | None = None,
record_as: str | None = None,
)
Bases: FlowProcess
Run one branch or the other depending on a condition.
Example::
ConditionalProcess(
condition="score < 0.7",
then_steps=[revise_step],
else_steps=[publish_step],
)
JSON::
{
"process_type": "conditional",
"process_name": "gate",
"condition": "score < 0.7",
"then": [ ...processes... ],
"else": [ ...processes... ],
"record_as": "needed_revision"
}
Initialize a conditional process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
Condition | Mapping[str, Any] | str
|
A condition object, config mapping, or bare expression string evaluated against the results mapping. |
required |
process_name
|
str
|
Name identifier for the process. |
'ConditionalProcess'
|
agent
|
BaseToolAgent
|
Default agent lent to children that have none. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
then_processes
|
Sequence[Process] | None
|
Processes run when the condition holds. |
None
|
else_processes
|
Sequence[Process] | None
|
Processes run when it does not. |
None
|
then_steps
|
Sequence[ProcessStep] | None
|
Convenience steps wrapped into a sequential process. |
None
|
else_steps
|
Sequence[ProcessStep] | None
|
Convenience steps for the else branch. |
None
|
record_as
|
str | None
|
Optional results key recording which branch was taken. |
None
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Evaluate the condition and run the matching branch.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this conditional process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "ConditionalProcess"
Restore a conditional process from JSON.
LoopProcess
LoopProcess(
condition: (
Condition | Mapping[str, Any] | str | None
) = None,
mode: str = "until",
max_iterations: int = DEFAULT_MAX_ITERATIONS,
process_name: str = "LoopProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
processes: Sequence[Process] | None = None,
steps: Sequence[ProcessStep] | None = None,
iteration_var: str = "iteration",
iterations_key: str | None = None,
on_max_iterations: str = "stop",
)
Bases: FlowProcess
Repeat a body until a condition is satisfied or a cap is reached.
The two modes differ in when the condition is tested and in what it means. They are inverses, as in most languages, so the same expression cannot simply be moved from one to the other:
"until" (the default)
The condition is a stop condition. Run the body, then test; stop when
it becomes true. The body always runs at least once, which is what
a refine-until-good-enough loop needs, because the condition usually
reads a value the body produces.
"while"
The condition is a continue condition. Test before each iteration;
stop when it becomes false. The body may run zero times, and the
condition must only reference values that already exist.
So mode="until", condition="approved" and
mode="while", condition="not approved" express the same loop.
max_iterations is always enforced, so a condition that never becomes
true cannot spin forever burning API credit.
JSON::
{
"process_type": "loop",
"process_name": "refine",
"mode": "until",
"condition": "contains(lower(review), 'approved')",
"max_iterations": 4,
"on_max_iterations": "stop",
"processes": [ ...body... ]
}
Initialize a loop process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
Condition | Mapping[str, Any] | str | None
|
Stop condition. When omitted the loop runs exactly
|
None
|
mode
|
str
|
|
'until'
|
max_iterations
|
int
|
Hard cap on iterations. Must be positive. |
DEFAULT_MAX_ITERATIONS
|
process_name
|
str
|
Name identifier for the process. |
'LoopProcess'
|
agent
|
BaseToolAgent
|
Default agent lent to children that have none. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
processes
|
Sequence[Process] | None
|
Body processes. |
None
|
steps
|
Sequence[ProcessStep] | None
|
Convenience steps wrapped into a sequential body process. |
None
|
iteration_var
|
str
|
Results key holding the zero-based iteration index,
so prompt templates can reference |
'iteration'
|
iterations_key
|
str | None
|
Results key receiving the total iteration count.
Defaults to |
None
|
on_max_iterations
|
str
|
|
'stop'
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Run the body repeatedly according to mode and condition.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this loop process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "LoopProcess"
Restore a loop process from JSON.
MapProcess
MapProcess(
items: str | SafeExpression,
process_name: str = "MapProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
processes: Sequence[Process] | None = None,
steps: Sequence[ProcessStep] | None = None,
item_var: str = "item",
index_var: str = "index",
collect: str | None = None,
result_key: str | None = None,
)
Bases: FlowProcess
Run a body once per item of a list, collecting the outputs.
Each iteration runs against its own copy of the results mapping, so an iteration rebinding a key cannot affect the next one, and only the collected list is written back to the outer results. The copy is shallow: a body that mutates a nested list or dict in place still affects the outer value and later iterations. Rebind rather than mutate.
JSON::
{
"process_type": "map",
"process_name": "per_topic",
"items": "topics",
"item_var": "topic",
"collect": "draft",
"result_key": "drafts",
"processes": [ ...body... ]
}
items is a sandboxed expression, so "topics" and
"topics[:3]" are both valid. When collect names a results key, the
output is the list of that key's value per iteration. When it is omitted,
each entry is a dict of whatever keys that iteration added.
Initialize a map process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
str | SafeExpression
|
Sandboxed expression selecting the list to iterate. |
required |
process_name
|
str
|
Name identifier for the process. |
'MapProcess'
|
agent
|
BaseToolAgent
|
Default agent lent to children that have none. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
processes
|
Sequence[Process] | None
|
Body processes run per item. |
None
|
steps
|
Sequence[ProcessStep] | None
|
Convenience steps wrapped into a sequential body process. |
None
|
item_var
|
str
|
Results key holding the current item. |
'item'
|
index_var
|
str
|
Results key holding the zero-based index. |
'index'
|
collect
|
str | None
|
Results key gathered from each iteration. |
None
|
result_key
|
str | None
|
Where the collected list lands. Defaults to
|
None
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Run the body once per item, collecting outputs into a list.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this map process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "MapProcess"
Restore a map process from JSON.
ParallelProcess
ParallelProcess(
branches: Sequence[Process] | None = None,
process_name: str = "ParallelProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
max_workers: int | None = None,
on_conflict: str = "error",
)
Bases: FlowProcess
Run independent branches concurrently and merge their results.
Each branch runs against its own copy of the results mapping in a worker thread; afterwards the keys each branch added or changed are merged back.
.. warning::
An agent instance is not thread-safe: ``ChatToolAgent`` keeps a
``last_messages_buffer`` on ``self``, so two branches sharing one agent
will interleave their transcripts. The ``.response`` text each step
stores stays correct, but ``ChatResponse.messages`` does not. Give each
branch its own agent when the transcript matters; this process warns
once if branches would share one.
JSON::
{
"process_type": "parallel",
"process_name": "research",
"branches": [ ...one process per branch... ],
"max_workers": 4,
"on_conflict": "error"
}
Under on_conflict="section" a contested output moves into a
sub-section named for its branch, so it is addressed by structure rather
than by a mangled name::
{outputs/news/draft} in a prompt template
outputs['news']['draft'] in a condition
Any value the key already held at the top of outputs is untouched.
Initialize a parallel process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branches
|
Sequence[Process] | None
|
Processes run concurrently, one per branch. |
None
|
process_name
|
str
|
Name identifier for the process. |
'ParallelProcess'
|
agent
|
BaseToolAgent
|
Default agent lent to branches that have none. See the thread-safety warning above. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
max_workers
|
int | None
|
Thread pool size. Defaults to the branch count. |
None
|
on_conflict
|
str
|
What to do when two branches write the same output
with different values: |
'error'
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Run every branch concurrently and merge the results.
branch_labels
Return a unique, template-safe label for each branch.
Labels are used to qualify keys under on_conflict="prefix". They
must contain only word characters, because MessageTemplate
substitutes on \w+ and the condition sandbox rejects anything with
a dot as attribute access — a qualified key that no prompt or condition
could read would be worse than useless.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this parallel process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "ParallelProcess"
Restore a parallel process from JSON.
Conditions
Conditions are compiled from a whitelisted subset of Python's grammar, never
eval'd, so pipeline JSON from an untrusted source cannot execute arbitrary
code.
SafeExpression
A compiled, sandboxed expression evaluated against a results mapping.
The expression is validated once at construction time, so an invalid or unsafe expression fails when the pipeline is loaded rather than midway through an expensive run.
referenced_names
Return the result keys this expression reads.
Only names used as values count. A name used solely as a call target
(len in len(draft)) is a helper, not a result key.
evaluate
Evaluate the expression against results and return its value.
Names resolve lazily, so and / or and conditional expressions
short-circuit properly: "has_score and score > 3" is well defined
even when score does not exist. Reaching a name that is genuinely
absent raises an error naming the results that do exist.
Condition
Bases: ABC
A serializable boolean test over the pipeline results mapping.
evaluate
abstractmethod
Return the truth value of this condition for results.
to_dict
abstractmethod
Return a JSON-compatible representation of this condition.
from_dict
abstractmethod
classmethod
Restore a condition of this kind from its JSON representation.
ExpressionCondition
ExpressionCondition(expression: str | SafeExpression)
Bases: Condition
A condition backed by a sandboxed expression over the results mapping.
Example::
ExpressionCondition("score > 0.8 and not is_empty(draft)")
condition_from_config
Build a Condition from JSON, a bare expression string, or itself.
A plain string is treated as an expression, so "score > 0.8" and
{"kind": "expression", "expression": "score > 0.8"} are equivalent.
register_condition_kind
Register a Condition subclass so JSON can dispatch to it.
Usable as a decorator. The class must set a non-empty kind.
Declaring agents and endpoints
A pipeline document can name the providers it runs against. API keys are never serialized — a config names the environment variable holding one.
ProviderConfig
ProviderConfig
dataclass
ProviderConfig(
provider_type: str,
model: str,
base_url: str | None = None,
api_key_env: str | None = None,
provider_identifier: str | None = None,
settings: dict[str, Any] = dict(),
extra_settings: dict[str, Any] = dict(),
env_file: str | None = None,
timeout: float | None = None,
max_retries: int | None = None,
)
A serializable description of a chat provider endpoint.
Attributes:
| Name | Type | Description |
|---|---|---|
provider_type |
str
|
One of :data: |
model |
str
|
Model identifier passed to the provider. |
base_url |
str | None
|
Optional endpoint override. This is what makes an OpenAI-shaped or Anthropic-shaped API reachable at a custom address (a gateway, a proxy, or a self-hosted server). |
api_key_env |
str | None
|
Environment variable holding the API key. Defaults to the provider's conventional variable. |
provider_identifier |
str | None
|
Optional override for the provider identifier string, where the provider supports one. |
settings |
dict[str, Any]
|
Sampling settings applied to the provider's defaults. Names must already exist on the provider, so typos are caught loudly. |
extra_settings |
dict[str, Any]
|
Additional request settings to add, for parameters a given endpoint understands but the provider does not declare. |
env_file |
str | None
|
Optional |
timeout |
float | None
|
Seconds to wait for a response before giving up. The SDKs default to 600 with two retries, so a stalled request can hang for half an hour looking like a crash; set this for anything running unattended. |
max_retries |
int | None
|
How many times to retry a failed request. |
resolve_spec
resolve_spec() -> (
tuple[ProviderSpec, str | None, str, bool]
)
Return the spec, base URL, API key env var, and key-optional flag.
resolve_api_key
Read the API key from the environment, or fail with a clear message.
apply_settings
Apply configured sampling settings to provider's defaults.
ProviderSettings.__setattr__ silently creates a dead attribute for
an unknown name, so settings are applied through set_value and an
unknown name is reported rather than quietly ignored.
to_dict
Return a JSON-compatible representation. Never contains a secret.
AgentConfig
dataclass
AgentConfig(
name: str,
provider: ProviderConfig,
agent_type: str = "chat_tool_agent",
)
ProviderSpec
dataclass
LazyAgentRegistry
LazyAgentRegistry(configs: Sequence[AgentConfig])
Bases: Mapping
Build declared agents on first reference, not on load.
Building eagerly means a document that merely declares an unused Anthropic agent fails to load with "ANTHROPIC_API_KEY is not set", even when every process references the OpenAI one. Constructing on demand keeps the failure attached to the agent actually being used.
register_provider_spec
register_provider_spec(spec: ProviderSpec) -> ProviderSpec
Register a provider kind so pipeline JSON can name it.
Sources and sinks
A source loads data into the inputs section; a sink emits a value out of the
results. Both are processes, so they nest inside flow control. Writing is gated
behind allow_writes — see the
sources and sinks guide.
SourceProcess
SourceProcess(
source: Source | Mapping[str, Any],
result_key: str = "documents",
splitter: Mapping[str, Any] | str | None = None,
section: str = "inputs",
process_name: str = "SourceProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
)
Bases: Process
Load data into the pipeline.
The loaded value lands in inputs by default, because it is input --
outputs stays for what the model produced.
JSON::
{
"process_type": "source",
"process_name": "load",
"source": {"type": "folder", "path": "./notes", "glob": "*.md"},
"splitter": {"type": "recursive_character", "chunk_size": 800},
"result_key": "documents"
}
Initialize a source process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source | Mapping[str, Any]
|
Source object or config describing what to load. |
required |
result_key
|
str
|
Key the loaded value is written to. |
'documents'
|
splitter
|
Mapping[str, Any] | str | None
|
Optional text splitter config. A source yielding text
becomes a list of chunks; a source yielding records becomes
more records, each with a |
None
|
section
|
str
|
Results section to write into. Defaults to |
'inputs'
|
process_name
|
str
|
Name identifier for the process. |
'SourceProcess'
|
agent
|
BaseToolAgent
|
Unused; sources call no model. Accepted so a source can sit anywhere a process can. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Load the source and write it into the configured section.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this source process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "SourceProcess"
Restore a source process from JSON.
SinkProcess
SinkProcess(
sink: Sink | Mapping[str, Any] | str,
source_key: str = "outputs",
process_name: str = "SinkProcess",
agent: BaseToolAgent = None,
agent_name: str | None = None,
record_as: str | None = None,
allow_writes: bool = True,
)
Bases: Process
Send a value out of the pipeline.
JSON::
{
"process_type": "sink",
"process_name": "save",
"sink": {"type": "file", "path": "out/{inputs/name}.md"},
"from": "outputs/draft"
}
Sinks that write a file or make a request are refused unless the pipeline
was loaded with allow_writes=True. A sink built in Python is permitted
by default; the gate is on loading a document that writes.
Initialize a sink process.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sink
|
Sink | Mapping[str, Any] | str
|
Sink object or config describing where the value goes. |
required |
source_key
|
str
|
Results path to read, such as |
'outputs'
|
process_name
|
str
|
Name identifier for the process. |
'SinkProcess'
|
agent
|
BaseToolAgent
|
Unused; sinks call no model. |
None
|
agent_name
|
str | None
|
Name of a declared agent, for JSON round-tripping. |
None
|
record_as
|
str | None
|
Optional output key recording what the sink returned -- the path written, or the HTTP status. |
None
|
allow_writes
|
bool
|
Whether this sink may touch the filesystem or the
network. Defaults to true in Python and false when loaded from
JSON without |
True
|
run_process
run_process(results: PipelineResults) -> PipelineResults
Read the configured value and emit it.
to_dict
to_dict(
tool_registry: PipelineToolRegistry | None = None,
) -> dict[str, Any]
Serialize this sink process.
from_dict
classmethod
from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "SinkProcess"
Restore a sink process from JSON.
Source types
Source
Bases: ABC
Loads data into a pipeline.
TextSource
FileSource
FilesSource
FolderSource
FolderSource(
path: str,
glob: str = "*",
encoding: str = "utf-8",
max_files: int = DEFAULT_MAX_FILES,
)
Bases: Source
Every file in a folder matching a glob, loaded as records.
Results are sorted by path so a run is reproducible.
register_source_type
Register a Source subclass so pipeline JSON can name it.
Sink types
Sink
Bases: ABC
Emits a value out of a pipeline.
emit
abstractmethod
emit(value: Any, results: PipelineResults) -> Any
Emit value. Returns anything worth recording, or None.
StreamSink
FileSink
FilesSink
FilesSink(
path: str,
content_key: str | None = None,
encoding: str = "utf-8",
create_parents: bool = True,
)
Bases: Sink
Write one file per item of a list.
The path template sees {index} and, when the item is a mapping, its
own keys — so "out/{name}.md" works over records from a folder source.
HttpSink
HttpSink(
url: str,
method: str = "POST",
headers: Mapping[str, str] | None = None,
headers_from_env: Mapping[str, str] | None = None,
as_json: bool = True,
field: str = "content",
timeout: float = 30.0,
)
Bases: Sink
Send the value to an HTTP endpoint.
Secrets are never serialized: headers_from_env maps a header name to
the environment variable holding its value, the same rule provider
configs follow.
register_sink_type
Register a Sink subclass so pipeline JSON can name it.
Path placeholders
render_path
Fill {section/key} placeholders in a path or URL.
Unresolved placeholders are an error rather than a silent gap: a path with a hole in it would write to the wrong place, which is exactly the mistake worth being loud about.
normalize_placeholders
Repair placeholders whose separator was rewritten by pathlib.
Path("out") / "{vars/index}.md" becomes out\{vars\index}.md on
Windows: joining a path normalizes every /, including the one inside
the placeholder, which then no longer resolves. Since no section or key
name contains a backslash, one inside braces is always this accident.
Only the inside of {...} is touched, so real Windows paths around it
are left alone.
Chunking
build_splitter
Build a text splitter from a config mapping, or a bare type name.
Returns None when config is None, meaning "do not split".
SplitterSpec
dataclass
How to construct one kind of text splitter from configuration.
register_splitter_spec
register_splitter_spec(spec: SplitterSpec) -> SplitterSpec
Register a splitter kind so pipeline JSON can name it.
Tools
PipelineToolRegistry
Resolve pipeline JSON tool references to live FunctionTool objects.
register_plugin
register_plugin(
name: str,
tools: (
Iterable[FunctionTool | type[BaseModel]]
| ToolRegistry
),
source: str | None = None,
) -> "PipelineToolRegistry"
Register tools under a plugin name and optional import source.
load_plugin
Import and register tools from a "module:attribute" source.
load_plugins
Load plugin declarations from pipeline JSON metadata.
resolve_tool
resolve_tool(
reference: Mapping[str, Any] | str,
) -> FunctionTool
Resolve a serialized tool reference.
reference_for_tool
reference_for_tool(tool: FunctionTool) -> dict[str, str]
Return the JSON reference for a registered tool.
to_plugin_configs
Return JSON-compatible plugin declarations for importable plugins.
get_tool
get_tool(
plugin_name: str, tool_name: str
) -> FunctionTool | None
Return a registered tool by plugin and public tool name.
PipelineToolPlugin
dataclass
PipelineToolPlugin(
name: str,
tools: Sequence[FunctionTool],
source: str | None = None,
)
A named source of tools that can be referenced from pipeline JSON.
The optional source uses "module:attribute" syntax. The attribute can
be an iterable of tools, a ToolRegistry, a Pydantic model class, or a
zero-argument factory returning any of those values.
load_pipeline_tools_from_spec
load_pipeline_tools_from_spec(
spec: str,
) -> list[FunctionTool]
Load tools from "module:attribute" for pipeline plugins.
Loading and extension
PipelineLoadContext
dataclass
PipelineLoadContext(
tool_registry: "PipelineToolRegistry | None" = None,
default_agent: BaseToolAgent | None = None,
process_agents: Mapping[str, BaseToolAgent] = dict(),
step_agents: Mapping[str, BaseToolAgent] = dict(),
load_tool_plugins: bool = True,
named_agents: Mapping[str, BaseToolAgent] = dict(),
json_default_agent_name: str | None = None,
allow_writes: bool = False,
ignore_agent_names: bool = False,
process_path: tuple[str, ...] = (),
parent_agent: BaseToolAgent | None = None,
)
Everything a process needs in order to rebuild itself from JSON.
Flow-control processes contain other processes, so deserialization is recursive. Rather than thread five separate keyword arguments down every level, the loader carries this single context and hands it to each child.
Agent resolution follows one rule: an agent injected from Python wins over a name declared in JSON at the same level of specificity, and a more specific source wins over a less specific one. In descending priority:
step_agentsentry for this step- the step's own JSON
agentname process_agentsentry for this process- the process's own JSON
agentname - the agent resolved for an enclosing flow-control process
default_agentpassed from Python- the JSON
default_agentname
nested
nested(
process_name: str, agent: BaseToolAgent | None = None
) -> "PipelineLoadContext"
Return a copy of this context scoped inside process_name.
named_agent
named_agent(
name: str | None, *, referenced_by: str
) -> BaseToolAgent | None
Resolve a JSON agent reference, or None when name is None.
agent_for_process
agent_for_process(
process_name: str, agent_name: str | None = None
) -> BaseToolAgent | None
Return the agent a process should use, honouring the priority rule.
agent_for_step
agent_for_step(
process_name: str,
step_name: str,
agent_name: str | None = None,
) -> BaseToolAgent | None
Return the agent a step should use, or None to inherit.
None means "no step-specific agent"; the process-level agent is
used at run time instead.
register_process_type
Register a Process subclass so pipeline JSON can dispatch to it.
Usable as a decorator. The class must define a non-empty process_type.
get_process_type
Return the registered Process subclass for process_type.
process_from_dict
process_from_dict(
data: Mapping[str, Any], context: PipelineLoadContext
) -> "Process"
Rebuild a single process from its JSON representation.
Errors
PipelineSerializationError
Bases: ValueError
Raised when a pipeline cannot be serialized or restored.
PipelineExecutionError
Bases: RuntimeError
Raised when a pipeline fails while running, not while loading.
Kept distinct from :class:PipelineSerializationError so that code
wrapping from_dict and code wrapping run_pipeline can catch the
failures that actually belong to each.
PipelineConditionError
Bases: ValueError
Raised when a condition cannot be compiled or evaluated.
AgentConfigurationError
Bases: ValueError
Raised when an agent or provider cannot be built from configuration.