Step¶
The unit a Plan is built from. Each Step declares a target (an
agent, a callable, or a tool name), the task it runs, where its
input comes from, what types it expects, and whether to persist its
payload. Routing is also declared on the step — see
Routing for the full surface.
Signature¶
from lazybridge import Step, from_prev
Step(
target, # str (tool name) | Callable | Agent
task=from_prev, # Sentinel or literal string — the prompt for this step
context=None, # Sentinel | str | list[Sentinel | str] — side context
sources=(), # iterable of objects with .text() (live-view injection)
writes=None, # str — Store key the payload is persisted under
input=Any, # type annotation for the step's input (informational)
output=str, # type for Envelope.payload (Pydantic class enables validation)
parallel=False, # mark as a member of a parallel band
name=None, # unique within the Plan; defaults to target's name
# Routing — see Routing guide for the full surface.
routes=None, # dict[str, Callable[[Envelope], bool]]
routes_by=None, # str — name of a Literal field on `output` model
after_branches=None, # str — exclusive-branch rejoin point
)
task=from_prev is the default and means "feed me whatever the
previous step produced". A literal string is used verbatim — useful
for hard-coded prompts at intermediate steps where the data flows
through context= instead.
Synopsis¶
A Step is the smallest declarative unit Plan orchestrates. Its
fields fall into three groups:
Data flow. target is what runs; task= is the prompt;
context= is the side data; sources= is the live-view objects
(e.g. a Store or Memory whose current state should be appended
verbatim). All four flow into the step's Envelope before the
target runs.
Typing and persistence. input= and output= declare the
step's expected input and output types; output=PydanticModel
enables validation and unlocks routes_by="field". writes="key"
persists the step's payload to store["key"] after a successful
run — required for checkpoint resume and for downstream agents
reading via sources=[store].
Concurrency and naming. parallel=True marks the step as a
member of a concurrent band (consecutive parallel=True steps are
dispatched together via asyncio.gather). name= is the
authoritative key the rest of the plan references — duplicates and
typos surface at construction time.
Routing fields (routes, routes_by, after_branches) get
their own dedicated guide: Routing.
When to use specific fields¶
task=literal string — for specialised steps where the prompt is fixed and the data flows throughcontext=. Example:Step("rank", task="Rank by relevance.", context=from_prev).task=sentinel — for delegating steps where the previous step's output is the prompt. Defaultfrom_previs appropriate most of the time.context=single sentinel — pull data from one upstream step.context=[...]list — synthesise from multiple upstream steps without an intermediate combiner. Items resolve independently and join with blank-line separators (same shape assources); literal strings can ride along to inject fixed boilerplate.sources=— for live-view state that should reflect the most recent value at step execution time (Store,Memory, any object with.text()). Sentinels resolve once at the start of the step; sources re-materialise on every read.output=Model— when the next step'scontext=will read a typed payload, or when you want compile-time validation of aroutes_by="field"reference.writes="key"— for crash recovery (resume=Truereconstructs from store writes) and for cross-agent reads (a downstream agent withsources=[store]sees the live key).parallel=True— when the step has no data dependency on its declared neighbours and concurrent execution is safe.
When NOT to use specific fields¶
- Don't set
output=Modelpurely for "type docs". It activates Pydantic validation, structured-output retry, androutes_bysemantics. If you want documentation, usedescription=or a comment. - Don't use
writes=for in-Plan-only data. A downstream step can read upstream output viafrom_step("name")directly from the in-memory history — noStoreneeded unless the value must also survive a crash or be visible to other agents. - Don't set
name=to a string that collides with a tool. When the target is a string (Step("research")), the framework resolves it against the wrapping agent'stools=[...]map. The step'sname=and the resolved tool's name are the same key — there's no separate "step name" to disambiguate.
Example¶
from pydantic import BaseModel
from lazybridge import Agent, LLMEngine, Plan, Step, Store, from_prev, from_step
class Hits(BaseModel):
items: list[str]
class Ranked(BaseModel):
top: list[str]
def normalise(text: str) -> str:
"""Strip and lowercase — pure Python, no LLM."""
return text.strip().lower()
searcher = Agent(
engine=LLMEngine("deepseek-v4-flash"),
name="search",
)
ranker = Agent(
engine=LLMEngine("deepseek-v4-flash"),
name="rank",
)
writer = Agent(
engine=LLMEngine("gpt-5.4-mini"),
name="write",
)
# 1) Mixed step targets — agents, plain callables, tool names by string.
plan = Agent(
engine=Plan(
Step(searcher, name="search"), # Agent target
Step(normalise, name="clean", task=from_prev), # plain callable target
Step("score", name="score", task=from_prev), # tool-name string target
Step(writer, name="write",
task="Write a 150-word brief.",
context=from_step("clean")),
),
tools=[score_tool], # the "score" tool name resolves here
)
# 2) Multi-source synthesis with context=[...].
class Brief(BaseModel):
title: str
body: str
synth = Agent(
engine=LLMEngine("deepseek-v4-flash"),
name="synth",
output=Brief,
)
policy_loader = Agent(
engine=LLMEngine("deepseek-v4-flash"),
name="policy",
)
competitor = Agent(
engine=LLMEngine("deepseek-v4-flash"),
name="bench",
)
plan = Agent(
engine=Plan(
Step(searcher, name="search", writes="hits", output=Hits),
Step(policy_loader, name="policy", task="Load the 2026 acceptable-use policy."),
Step(competitor, name="bench", task="Find three relevant prior posts."),
Step(synth, name="synth",
task="Draft a 300-word brief; cite each source explicitly.",
context=[
from_step("search"),
from_step("policy"),
from_step("bench"),
"Style: neutral, third-person, no superlatives.",
],
output=Brief),
),
)
# 3) Live-view sources — a writer reads any prior reviewer verdict
# on every loop iteration.
class Verdict(BaseModel):
feedback: str
approved: bool
store = Store()
plan = Agent(
engine=Plan(
Step(writer, name="write",
task="Draft a 200-word answer. If a 'verdict' is in the store, "
"rewrite the previous draft addressing the feedback.",
sources=[store], # live read on every run
writes="draft"),
Step(reviewer, name="review",
task="Score the draft.",
context=from_prev,
output=Verdict,
writes="verdict"),
),
store=store,
)
Pitfalls¶
task=stris a literal, not a sentinel reference.task="from_prev"puts the literal string"from_prev"into the step's task. Use the importedfrom_prevsymbol.output=Modelis for typing the payload, not for routing. Anextfield on the model is just a regular field. To declare routing, setroutes={...}orroutes_by="field"on the step (see Routing).writes=does not deduplicate. Two steps withwrites="result"overwrite the same key. Pick distinct keys or namespace them.Step(target=callable, name="...")doesn't get an LLM. The callable runs once with the step's task as its argument. Useful for normalisation, validation, or any deterministic transformation between LLM steps.Step("name")requires the name to resolve. If the wrapping agent has notools=[...]matching"name", the framework raisesPlanCompileErrorat construction. Either pass the agent intools=[...], or change the step toStep(target=agent)directly.parallel=Trueis bundled with consecutive parallel steps. The engine groups every adjacentparallel=Truestep into one band; a non-parallel step in between starts a new band. Keep parallel siblings contiguous in the declaration.- A failed parallel branch wipes the band's writes. No
writes=from the band are applied on error —resume=Truere-runs the whole band cleanly. Don't write side effects inside parallel steps that aren't crash-safe to repeat.
See also¶
- Plan — the engine that interprets a list of
Steps. - Sentinels —
from_prev/from_start/from_step/from_parallel/from_parallel_all/from_memory/from_agentsemantics in full. - Routing —
routes={...}predicates,routes_by="field"Literal dispatch,after_branches=rejoin points, and thewhenDSL. - Store —
writes=lands here;sources=[store]reads it back live. - Guides → Full → Parallel plan steps (Phase 3b) — concurrent
bands and
from_parallel_allaggregation.