Workflows
Orchestrate agents in sequential, parallel, routed, and looped pipelines with a single AgentFlow call
from praisonaiagents import Agent, AgentFlow
researcher = Agent(name="Researcher", instructions="Research the given topic thoroughly.")
writer = Agent(name="Writer", instructions="Write a concise summary of the research.")
workflow = AgentFlow(steps=[researcher, writer])
result = workflow.start("Latest breakthroughs in AI agents")
print(result["output"])AgentFlow runs agents as sequential steps, passing each output into the next as context.
Feed a run some free-text context with start(content="…") under process="workflow".
from praisonaiagents import Agent, Task, PraisonAIAgents
analyst = Agent(name="Analyst", instructions="Analyse the provided context and summarise it.")
analyse = Task(name="analyse", description="Summarise the background material", agent=analyst)
team = PraisonAIAgents(
agents=[analyst],
tasks=[analyse],
process="workflow",
)
team.start(content="Q3 revenue rose 12% while support tickets fell 8%.")Quick Start
- Sequential pipeline
python from praisonaiagents import Agent, AgentFlow analyst = Agent(name="Analyst", instructions="Analyze the topic and extract key insights.") strategist = Agent(name="Strategist", instructions="Develop actionable strategies from the analysis.") presenter = Agent(name="Presenter", instructions="Summarize strategies into a clear presentation.") workflow = AgentFlow(steps=[analyst, strategist, presenter]) result = workflow.start("Market trends in AI industry 2024") print(result["output"]) - Parallel research
python from praisonaiagents import Agent, AgentFlow, parallel market = Agent(name="Market", instructions="Research market trends.") competitor = Agent(name="Competitor", instructions="Research competitor landscape.") customer = Agent(name="Customer", instructions="Research customer needs.") aggregator = Agent(name="Aggregator", instructions="Synthesize all research into a unified report.") workflow = AgentFlow(steps=[ parallel([market, competitor, customer], max_workers=3), aggregator, ]) result = workflow.start("AI industry 2024") print(result["output"]) - Conditional routing
python from praisonaiagents import Agent, AgentFlow, WorkflowContext, StepResult, route tech = Agent(name="TechExpert", instructions="Answer technical questions in depth.") creative = Agent(name="Creative", instructions="Write engaging creative content.") general = Agent(name="General", instructions="Provide a helpful general-purpose answer.") def classify(ctx: WorkflowContext) -> StepResult: text = ctx.input.lower() if any(k in text for k in ("code", "python", "api", "debug")): return StepResult(output="technical") if any(k in text for k in ("story", "poem", "creative")): return StepResult(output="creative") return StepResult(output="default") workflow = AgentFlow(steps=[ classify, route({"technical": [tech], "creative": [creative], "default": [general]}), ]) result = workflow.start("Write a Python function to sort a list") print(result["output"])
Preview a Workflow Before It Runs
Call flow.to_mermaid() to render a workflow’s shape as a mermaid diagram — no run, no cost.
from praisonaiagents import Agent, AgentFlow
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a summary.")
flow = AgentFlow(steps=[researcher, writer], name="review flow")
print(flow.to_mermaid())See Workflow Diagram for how each step type renders.
How It Works
Each step receives the previous step’s output as context. Context passing is automatic — no extra code required.
Which Pattern to Use?
Pattern Helpers
| Pattern | Import | Purpose |
|---|---|---|
parallel(steps, max_workers=N) | from praisonaiagents import parallel | Run steps concurrently |
route({"label": [steps]}) | from praisonaiagents import route | Branch on step output |
loop(fn, over="var") | from praisonaiagents import loop | Iterate over a list variable |
repeat(fn, until=cond) | from praisonaiagents import repeat | Evaluator-optimizer loop |
Discussion([a, b], rounds=3) | from praisonaiagents import Discussion | N agents take turns on a shared thread |
when(condition, then_steps, else_steps) | from praisonaiagents import when | Conditional block |
include(workflow=wf) | from praisonaiagents import include | Embed a sub-workflow |
from praisonaiagents import AgentFlow, Agent, WorkflowContext, StepResult
from praisonaiagents import route, parallel, loop, repeat, when, include
agent_a = Agent(name="A", instructions="Handle task A.")
agent_b = Agent(name="B", instructions="Handle task B.")
worker = Agent(name="Worker", instructions="Process one item.")
sub_flow = AgentFlow(name="sub", steps=[agent_b])
def classify(ctx: WorkflowContext) -> StepResult:
return StepResult(output="a" if "a" in ctx.input.lower() else "b")
def process_item(ctx: WorkflowContext) -> StepResult:
item = ctx.variables.get("item", "")
return StepResult(output=f"Processed: {item}")
workflow = AgentFlow(
steps=[
classify,
route({"a": [agent_a], "default": [agent_b]}),
parallel([agent_a, agent_b], max_workers=2),
loop(process_item, over="items"),
repeat(worker, until=lambda ctx: len(ctx.previous_result or "") > 50, max_iterations=3),
when(condition="{{score}} > 80", then_steps=[agent_a], else_steps=[agent_b]),
include(workflow=sub_flow),
],
variables={"items": ["AI", "ML", "NLP"]},
)parallel() and max_workers
max_workers caps concurrent steps in parallel() and loop(parallel=True).
max_workers value | Effective concurrency |
|---|---|
None (default) | min(3, len(steps)) |
1 | Sequential — useful for rate-limited providers |
N ≤ len(steps) | N |
N > 3 | Honored; an info log is emitted |
Workflow Hooks
from praisonaiagents import Agent, AgentFlow, WorkflowHooksConfig
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a summary.")
workflow = AgentFlow(
steps=[researcher, writer],
hooks=WorkflowHooksConfig(
on_workflow_start=lambda w, i: print(f"🚀 Starting: {i}"),
on_step_start=lambda name, ctx: print(f"▶ {name}"),
on_step_complete=lambda name, r: print(f"✅ {name}: {str(r.output)[:60]}"),
on_step_error=lambda name, e: print(f"❌ {name}: {e}"),
on_workflow_complete=lambda w, r: print(f"🎉 Done: {r['status']}"),
),
)
result = workflow.start("AI trends 2024")WorkflowHooksConfig options:
| Option | Type | Default | Description |
|---|---|---|---|
on_workflow_start | Callable | None | Called once before the first step |
on_workflow_complete | Callable | None | Called once after the last step |
on_step_start | Callable | None | Called before each step |
on_step_complete | Callable | None | Called after each step succeeds |
on_step_error | Callable | None | Called when a step raises an exception |
Planning & Reasoning
from praisonaiagents import Agent, AgentFlow
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a summary.")
workflow = AgentFlow(
steps=[researcher, writer],
planning=True, # or a dict: {"llm": "gpt-4o", "reasoning": True}
)
result = workflow.start("Research and write about AI trends")Pass planning=True to enable it, or a dict to tune the planner:
workflow = AgentFlow(
steps=[researcher, writer],
planning={"llm": "gpt-4o", "reasoning": True},
)| Option | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | Enable planning mode |
llm | str | None | LLM for planning (defaults to workflow LLM) |
reasoning | bool | False | Enable chain-of-thought reasoning |
Advanced: WorkflowPlanningConfig class
from praisonaiagents import Agent, AgentFlow, WorkflowPlanningConfig
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a summary.")
workflow = AgentFlow(
steps=[researcher, writer],
planning=WorkflowPlanningConfig(enabled=True, llm="gpt-4o", reasoning=True),
)
result = workflow.start("Research and write about AI trends")Memory Integration
from praisonaiagents import Agent, AgentFlow
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Continue from previous research.")
workflow = AgentFlow(
steps=[researcher, writer],
memory={"backend": "chroma", "config": {"persist": True, "collection": "my_workflow"}},
)
result1 = workflow.start("Research AI trends")
result2 = workflow.start("Continue the research on LLMs")Pass memory=True for the default file backend, or a dict to pick a backend:
| Option | Type | Default | Description |
|---|---|---|---|
backend | str | "file" | Memory backend ("file", "chroma", "redis") |
user_id | str | None | User identifier for memory scoping |
session_id | str | None | Session identifier |
config | dict | None | Backend-specific configuration |
Advanced: WorkflowMemoryConfig class
from praisonaiagents import Agent, AgentFlow, WorkflowMemoryConfig
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Continue from previous research.")
workflow = AgentFlow(
steps=[researcher, writer],
memory=WorkflowMemoryConfig(backend="chroma", config={"persist": True, "collection": "my_workflow"}),
)
result = workflow.start("Research AI trends")Step-Result Caching
Cache a deterministic step’s output by its inputs so it is reused across runs instead of paying the full LLM cost every time.
from praisonaiagents import Agent, AgentFlow
from praisonaiagents.workflows.step_cache import InMemoryStepCache
formatter = Agent(name="Formatter", instructions="Format the text as clean JSON.")
flow = AgentFlow(steps=[formatter], cache=True) # simplest
flow = AgentFlow(steps=[formatter], cache=InMemoryStepCache(max_entries=256)) # tunedCaching is opt-in — only successful steps of a deterministic step are stored, and both linear and pattern executors are covered.
| Option | Type | Default | Description |
|---|---|---|---|
cache | Any | None | True for a process-scoped LRU, an InMemoryStepCache, or any object with get()/set() |
Cache key composition, correctness guarantees, and custom cache backends.
Guardrails & Validation
from praisonaiagents import AgentFlow, Task, WorkflowContext, StepResult
def generate_content(ctx: WorkflowContext) -> StepResult:
return StepResult(output=f"Article about: {ctx.input}")
def validate_output(result: StepResult):
if len(result.output) < 50:
return (False, "Content too short — please expand to at least 50 characters.")
return (True, None)
workflow = AgentFlow(steps=[
Task(
name="generator",
handler=generate_content,
guardrails=validate_output,
max_retries=3,
)
])
result = workflow.start("AI breakthroughs 2024")Output Modes
from praisonaiagents import Agent, AgentFlow
workflow = AgentFlow(
steps=[Agent(name="A", instructions="Answer the question.")],
output="silent",
)
result = workflow.start("What is AI?")from praisonaiagents import Agent, AgentFlow
workflow = AgentFlow(
steps=[Agent(name="A", instructions="Answer the question.")],
output="status",
)
result = workflow.start("What is AI?")from praisonaiagents import Agent, AgentFlow
workflow = AgentFlow(
steps=[Agent(name="A", instructions="Answer the question.")],
output="trace",
)
result = workflow.start("What is AI?")from praisonaiagents import Agent, AgentFlow
workflow = AgentFlow(
steps=[Agent(name="A", instructions="Answer the question.")],
output="verbose",
)
result = workflow.start("What is AI?")| Preset | Description |
|---|---|
silent | No output (default, lowest overhead) |
minimal | Final answer only |
status | Tool calls + final output inline |
trace | Timestamped execution trace |
verbose | Full Rich panels |
debug | Trace + metrics |
Async Execution
import asyncio
from praisonaiagents import Agent, AgentFlow
workflow = AgentFlow(steps=[
Agent(name="Researcher", instructions="Research the topic."),
Agent(name="Writer", instructions="Summarize the research."),
])
async def main():
result = await workflow.astart("AI trends 2024")
print(result["output"])
asyncio.run(main())Async step handlers (async def)
Any step handler can be async def — the workflow awaits it from both start() and astart(), and the awaited value becomes the step’s output and the next step’s ctx.previous_result.
import asyncio
from praisonaiagents import Workflow, StepResult
async def fetch_feed(ctx):
await asyncio.sleep(0.1)
return StepResult(output="latest news")
def summarize(ctx):
return StepResult(output=f"Summary of: {ctx.previous_result}")
result = Workflow(steps=[fetch_feed, summarize]).start("news")
print(result["output"]) # "Summary of: latest news"
print(result["status"]) # "completed"This applies to handlers in steps=[...] and inside loop(...), parallel(...), route(...), repeat(...), and if_(...).
Status Tracking
from praisonaiagents import Agent, AgentFlow, WorkflowContext, StepResult
def step1(ctx: WorkflowContext) -> StepResult:
return StepResult(output="step1 done")
def step2(ctx: WorkflowContext) -> StepResult:
return StepResult(output="step2 done")
workflow = AgentFlow(steps=[step1, step2])
result = workflow.start("input")
print(workflow.status) # "completed"
print(workflow.step_statuses) # {"step1": "completed", "step2": "completed"}Variable Substitution
Every action string processes placeholders in this order:
graph TB
A["action string"] --> B["Replace custom variables<br/>{{topic}}, {{step_name_output}}"]
B --> C{Previous output exists?}
C -->|No| F["Replace {{input}}<br/>(arg → YAML default_input → empty)"]
C -->|Yes| D{"Contains {{previous_output}}?"}
D -->|Yes| E1["Replace in place"]
D -->|No| E2["Auto-append as context"]
E1 --> F
E2 --> F
F --> G["Final prompt → Agent"]
classDef in fill:#6366F1,stroke:#7C90A0,color:#fff
classDef proc fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef dec fill:#189AB4,stroke:#7C90A0,color:#fff
classDef out fill:#10B981,stroke:#7C90A0,color:#fff
class A in
class B,E1,E2,F proc
class C,D dec
class G out| Placeholder | Resolves to |
|---|---|
{{your_variable}} | Value from variables={} or earlier step’s output_variable |
{{previous_output}} | Previous step output — auto-appended if omitted |
{{step_name_output}} | Named step output |
{{input}} | Value passed to workflow.start(...) / workflow.run(input=...), or the YAML top-level input: field (workflow.default_input) when no argument is provided. |
from praisonaiagents import Agent, AgentFlow, Task
researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a one-line tweet.")
workflow = AgentFlow(steps=[
Task(name="research", agent=researcher, action="Research {{input}}"),
Task(name="summary", agent=writer, action="Summarise: {{previous_output}}"),
Task(name="tweet", agent=writer, action="Write a one-line tweet."),
])
workflow.start("AI agents")WorkflowManager
WorkflowManager discovers and executes markdown-defined workflows from .praisonai/workflows/.
from praisonaiagents import Agent, WorkflowManager
agent = Agent(name="Assistant", instructions="Execute workflow steps.")
manager = WorkflowManager()
workflows = manager.list_workflows()
result = manager.execute(
workflow_name="deploy",
default_agent=agent,
variables={"environment": "staging"},
)
print(result)Checkpoints
Save progress with checkpoint= (saved after each step), continue an interrupted run with resume=, and — only when a real edit happened — override a fingerprint mismatch with rebase_checkpoint=True.
# Save a checkpoint after each completed step
result = manager.execute("research-pipeline", default_agent=agent,
checkpoint="my-checkpoint")
# Resume from where it stopped
result = manager.execute("research-pipeline", default_agent=agent,
resume="my-checkpoint")
print(result.get("resumed_from_step")) # 0-indexed int when resumed; absent otherwiseDefinition fingerprint. Each checkpoint stores a stable content hash of the workflow’s steps (each step’s name, action, agent name, and full agent config). Whitespace- or comment-only edits keep the same fingerprint, so a resume still works. Adding, removing, or reordering steps — or changing an agent config value such as instructions, model, tools, condition, or routing — produces a
new fingerprint.
Fail-closed on a missing checkpoint. Resuming onto a name that does not exist refuses to run rather than silently restarting from step 1:
result = manager.execute("research-pipeline", default_agent=agent,
resume="does-not-exist")
print(result["success"]) # False
print(result["error"]) # checkpoint 'does-not-exist' not found; refusing to resume. ...Fingerprint mismatch. After a real edit, resume refuses and the error names both fingerprints and points at the remediation:
workflow definition changed since the checkpoint (fingerprint mismatch:
checkpoint=a1b2c3d4e5f6, current=9f8e7d6c5b4a). Re-run without --resume to start
fresh, or pass --rebase-checkpoint to deliberately continue at the same step
index against the edited definition.Pass rebase_checkpoint=True to continue at the same step index against the edited workflow (a warning is logged naming both fingerprints):
result = manager.execute("research-pipeline", default_agent=agent,
resume="my-checkpoint", rebase_checkpoint=True)| Situation | Behaviour |
|---|---|
| Resume, no checkpoint by that name | Refuses to run; error names the missing checkpoint |
| Resume, file edited (whitespace / comments only) | Runs; same fingerprint |
| Resume, file edited (steps added/reordered, agent config changed) | Refuses; error names both fingerprints and the --rebase-checkpoint remediation |
Resume with rebase_checkpoint=True after a real edit | Runs at the same numeric step index; warning logged |
Loop step (loop_over) | Checkpoint written once after the whole loop completes; resume skips the loop entirely |
Async
aexecute() accepts the same checkpoint, resume, and rebase_checkpoint parameters (and returns the same resumed_from_step field) and supports loop_over and branch_condition routing with full parity to execute().
import asyncio
from praisonaiagents import Agent, WorkflowManager
agent = Agent(name="Assistant", instructions="Execute workflow steps.")
manager = WorkflowManager()
result = asyncio.run(
manager.aexecute("research-pipeline", default_agent=agent, variables={"topic": "AI"})
)
# Same checkpoint / resume as the sync path
result = asyncio.run(
manager.aexecute("research-pipeline", default_agent=agent, checkpoint="my-checkpoint")
)
result = asyncio.run(
manager.aexecute("research-pipeline", default_agent=agent, resume="my-checkpoint")
)Loop Steps: Error Handling & Checkpoints
A loop_over step runs its step once per item. Two behaviors apply on both execute() and aexecute():
- Stop-on-error aborts the workflow. When a
loop_overstep fails on any iteration and itson_error="stop"(theTaskdefault), the entire workflow aborts — not just the loop.result["success"]becomesFalseand no later steps run. - Completed loop steps checkpoint once. With
checkpoint=set, aloop_overstep persists a checkpoint after the whole loop completes (matching single-step behavior). Onresume=, the loop is not re-run — execution continues from the next step.
Error Handling Across Patterns
As of PraisonAI #4194, the same failure rules now apply inside every nested pattern (loop, parallel, when/if_, route, repeat), not just loop_over:
on_error="stop"(theTaskdefault) inside any nested pattern halts the whole workflow — the stop signal propagates out of the pattern to the top level.on_error="continue"keeps the workflow running, but the finalresult["status"]is"failed"if any step failed.- Downstream steps never receive the exception text as input; the error is recorded on the step, not folded into its output.
See Error Handling for the full guide, including parallel(on_failure=...) modes.
Workflow File Format
Markdown files with YAML frontmatter define multi-step workflows for WorkflowManager:
---
name: Research Pipeline
default_llm: gpt-4o-mini
planning: true
variables:
topic: AI trends
---
## Step 1: Research
Research the topic thoroughly.
```agent
role: Researcher
goal: Find comprehensive information
instructions: Expert researcher with broad domain knowledge
```
```action
Search for information about {{topic}}
```
output_variable: research_data
## Step 2: Analyze
```agent
role: Analyst
goal: Analyze data patterns
```
```action
Analyze: {{research_data}}
```Best Practices
Use specialized agents per step
Give each step a distinct agent with a focused role — Researcher, Analyst, Writer. Agents with clear instructions outperform generalist ones.
Set max_workers for parallel steps
LLM-backed parallel steps hit rate limits fast. Start with max_workers=3 and increase only after testing.
Name your steps for debugging
Use Task(name="...") for every step. Named steps appear in workflow.step_statuses and execution history.
Guardrails over hope
Add guardrails= to steps that produce structured output (reports, JSON, code). Short feedback loops via max_retries cost less than prompt engineering.
Prefer when() over complex routing
Use when(condition="{{score}} > 80", ...) for simple thresholds. Reserve route() for multi-branch decisions.
Keep steps atomic
One step → one responsibility. Easier to debug, retry, and replace.