Workflows
A workflow is a named unit of work. It owns payload validation, prompt construction, step sequencing, and result shaping. Callers know only its name — see Invocation contract.
Types
WorkflowDefinition protocol (agentcore/workflows/registry.py):
@runtime_checkable
class WorkflowDefinition(Protocol):
name: str
description: str
def run(self, payload: dict, options: RunOptions | None = None) -> WorkflowResult: ...
That is the entire requirement. Extending BaseWorkflow is optional and only useful
for multi-step workflows.
@dataclass
class WorkflowStep:
name: str # unique slug; used as a dependency key
prompt: str # supports {{variable}} placeholders
depends_on: list[str] = field(default_factory=list)
options: RunOptions | None = None # per-step overrides
output_schema: Any | None = None # Pydantic BaseModel subclass
@dataclass
class WorkflowStepResult:
name: str
output: str | None # None when the step failed
structured_output: Any = None
usage: dict[str, int] | None = None
error: str | None = None
@dataclass
class WorkflowResult:
success: bool
output: str
structured_output: Any = None
stop_reason: str | None = None
usage: dict[str, int] | None = None
error: str | None = None
steps: list[WorkflowStepResult] | None = None
WorkflowRegistry
class WorkflowRegistry:
def register(self, *workflows: WorkflowDefinition) -> WorkflowRegistry # chainable
def list(self) -> list[str]
def run(self, name, payload=None, options=None) -> WorkflowResult
-
register()raisesValueErroron a duplicate name — a collision fails startup. -
run()raisesKeyErrorwith a message listing the available workflows when the name is unknown. -
One registry per process, built once by
build_registry().
BaseWorkflow
agentcore/workflows/base_workflow.py. Register steps in the constructor, then call
execute_steps() from run().
class MyWorkflow(BaseWorkflow):
name = "my-workflow"
description = "Does the thing."
def __init__(self) -> None:
super().__init__(my_agent)
self.add_step(WorkflowStep(name="fetch", prompt="Fetch data for {{id}}."))
self.add_step(WorkflowStep(
name="analyse", prompt="Analyse the data above.",
depends_on=["fetch"], output_schema=AnalysisOutput,
))
def run(self, payload, options=None):
if not payload.get("id"):
return WorkflowResult(success=False, output="", error="id is required")
return self.execute_steps({"id": payload["id"]}, options)
add_step()
Chainable. Validates that every name in depends_on refers to an already
registered step, so forward references raise immediately at construction time —
which means at container startup, not at request time.
execute_steps()
Steps run in registration order against a single Strands Agent obtained from
self._agent.new_session(options). Because it is one Agent, agent.messages
accumulates across steps and each step sees the whole prior conversation with no
manual context threading.
Per step:
-
Verify every
depends_onstep has completed. Any missing one fails the workflow immediately withMissing dependencies: …. -
Substitute
{{variable}}placeholders fromvariables. -
When
output_schemais set, append a JSON-schema instruction to the prompt (derived frommodel_json_schema()) demanding a single JSON object with no markdown fences or prose. -
Call
agent(resolved_prompt). -
When
output_schemais set, strip any markdown fences and validate withmodel_validate_json(). Validation failure is swallowed — the raw text is kept andstructured_outputstaysNone. -
Accumulate token usage and append a
WorkflowStepResult. -
On an exception, stop immediately and return
success=Falsenaming the failing step.
Structured output
Two independent mechanisms, and it is worth knowing which is in play.
WorkflowStep.output_schema(this module)-
BaseWorkflowappends a schema instruction to the prompt text and validates the response itself. Prompt-level, best-effort, tolerant of failure. AgentConfig.structured_output_model(morpheus-llm)-
Strands adds a dedicated output tool, so the model is structurally constrained and
AgentResult.structured_outputcarries a validated instance. Stronger. Used by the Lambda agents. See structured output.
The dialler workflow uses the first; the enrichment, scoring, and email categorisation agents use the second.
Built-in workflows
PromptWorkflow
agentcore/workflows/builtin/prompt_workflow.py. Implements the protocol directly —
no BaseWorkflow. Takes an agent, a name, and a description at construction, so one
class serves any agent:
PromptWorkflow(dialler_agent, "test", "Sample test workflow")
Validates payload.prompt, calls self._agent.run(prompt, options), and wraps the
result. Exceptions become WorkflowResult(success=False, error=str(exc)).
DiallerAgentWorkflow
agentcore/workflows/builtin/dialler_workflow.py. Two steps.
Step 1 — analyse-history
Prompt: get contact information for {{phoneNumber}} from HubSpot; retrieve all
previous call history, transcripts, emails and other interactions; understand the
content and outcome of each; identify the last interaction, open tasks, and the
current relationship status.
class AnalyseHistoryOutput(BaseModel):
contact: _ContactInfo # name, email, phone, company,
# hubspot_id, lifecycle_stage, hubspot_url
last_interaction: _LastInteraction | None # date, type, summary
open_tasks: list[str] | None
relationship_status: str
interaction_history: list[_InteractionHistoryItem] | None # date, type, summary
_LastInteraction.type is constrained to
call | email | meeting | note | task | other.
Step 2 — prepare-brief
Depends on step 1. Produces a brief for a dialler agent, constrained to a
recommended_action of follow-up-call | email | sms | no-action and an urgency of
high | medium | low. Full schema in Invocation contract.
The tool work all happens in step 1 through the HubSpot MCP client; step 2 is pure reasoning over the conversation the agent already has in context.
build_registry()
def build_registry() -> WorkflowRegistry:
return WorkflowRegistry().register(
PromptWorkflow(dialler_agent, "test", "Sample test workflow"),
DiallerAgentWorkflow(),
)
Takes no arguments — agents and tools are self-contained module-level singletons. Every new workflow must be added here or it is unreachable.
Adding a workflow
-
Create
agentcore/workflows/builtin/<name>_workflow.py. -
Validate required payload keys at the top of
run()and returnWorkflowResult(success=False, …)for anything missing. -
Extend
BaseWorkflowfor multi-step; implement the protocol directly for single-step. -
Register it in
agentcore/workflows/init.py. -
Document the name and payload in Invocation contract.
Guidelines
-
Use descriptive step slugs. They appear in
WorkflowResult.stepsand in error messages:lookup-contact, notstep1. -
One task per step. If a step’s prompt has two verbs, it is two steps.
-
Pass runtime data through
variables, not by string-concatenating into prompts.{{placeholder}}substitution keeps the prompt readable and diffable. -
Declare
depends_onhonestly. It is the only thing that guards against a step running before the context it needs exists. -
Add an
output_schemato any step whose output is consumed programmatically — and handlestructured_output is None, because validation failure is non-fatal.