Workflows

Orchestrate agents in sequential, parallel, routed, and looped pipelines with a single AgentFlow call

python
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".

python
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%.")
Diagram

input

research

output

👤 User

🔍 Researcher

✍️ Writer

Quick Start

  1. 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"])
  2. 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"])
  3. 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.

python
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

Diagram
Step NStep 2Step 1AgentFlow👤 UserStep NStep 2Step 1AgentFlow👤 Userworkflow.start(“input”)inputoutput₁output₁ as contextoutput₂output₂ as contextoutputₙresult[“output”]

Each step receives the previous step’s output as context. Context passing is automatic — no extra code required.


Which Pattern to Use?

Diagram

What do you need?

Run agents one after another

Run agents at the same time

Choose different agents based on content

Process a list of items

Retry until quality threshold met

Conditional — skip or execute block

Reuse another workflow

N agents take turns on a thread

Sequential steps list

parallel()

classify fn + route()

loop()

repeat()

when()

include()

Discussion


Pattern Helpers

PatternImportPurpose
parallel(steps, max_workers=N)from praisonaiagents import parallelRun steps concurrently
route({"label": [steps]})from praisonaiagents import routeBranch on step output
loop(fn, over="var")from praisonaiagents import loopIterate over a list variable
repeat(fn, until=cond)from praisonaiagents import repeatEvaluator-optimizer loop
Discussion([a, b], rounds=3)from praisonaiagents import DiscussionN agents take turns on a shared thread
when(condition, then_steps, else_steps)from praisonaiagents import whenConditional block
include(workflow=wf)from praisonaiagents import includeEmbed a sub-workflow
python
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).

Diagram

How many parallel steps?

1–3 LLM-backed

4–10 LLM-backed

Many pure-compute

Leave max_workers unset
(default: min 3, len steps)

max_workers=N + rate limiter

max_workers=N — no limiter needed

max_workers valueEffective concurrency
None (default)min(3, len(steps))
1Sequential — useful for rate-limited providers
N ≤ len(steps)N
N > 3Honored; an info log is emitted

Workflow Hooks

Diagram

next step

workflow.start

on_workflow_start

on_step_start

on_step_complete

on_workflow_complete

python
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:

OptionTypeDefaultDescription
on_workflow_startCallableNoneCalled once before the first step
on_workflow_completeCallableNoneCalled once after the last step
on_step_startCallableNoneCalled before each step
on_step_completeCallableNoneCalled after each step succeeds
on_step_errorCallableNoneCalled when a step raises an exception

Planning & Reasoning

Diagram

Input

🧠 Planner LLM

Execution Plan

Step 1

Step 2

Output

python
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:

python
workflow = AgentFlow(
    steps=[researcher, writer],
    planning={"llm": "gpt-4o", "reasoning": True},
)
OptionTypeDefaultDescription
enabledboolTrueEnable planning mode
llmstrNoneLLM for planning (defaults to workflow LLM)
reasoningboolFalseEnable chain-of-thought reasoning
Advanced: WorkflowPlanningConfig class
python
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

Diagram

Run 1

Memory Store

Run 2

python
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:

OptionTypeDefaultDescription
backendstr"file"Memory backend ("file", "chroma", "redis")
user_idstrNoneUser identifier for memory scoping
session_idstrNoneSession identifier
configdictNoneBackend-specific configuration
Advanced: WorkflowMemoryConfig class
python
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.

python
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))    # tuned

Caching is opt-in — only successful steps of a deterministic step are stored, and both linear and pattern executors are covered.

OptionTypeDefaultDescription
cacheAnyNoneTrue for a process-scoped LRU, an InMemoryStepCache, or any object with get()/set()
Step-Result Cache

Cache key composition, correctness guarantees, and custom cache backends.


Guardrails & Validation

Diagram

valid

invalid

Step Output

Guardrail

Next Step

Retry with feedback

python
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

Diagram

Workflow

silent

status

trace

verbose

silent (default)
from praisonaiagents import Agent, AgentFlow

workflow = AgentFlow(
    steps=[Agent(name="A", instructions="Answer the question.")],
    output="silent",
)
result = workflow.start("What is AI?")
status
from praisonaiagents import Agent, AgentFlow

workflow = AgentFlow(
    steps=[Agent(name="A", instructions="Answer the question.")],
    output="status",
)
result = workflow.start("What is AI?")
trace
from praisonaiagents import Agent, AgentFlow

workflow = AgentFlow(
    steps=[Agent(name="A", instructions="Answer the question.")],
    output="trace",
)
result = workflow.start("What is AI?")
verbose
from praisonaiagents import Agent, AgentFlow

workflow = AgentFlow(
    steps=[Agent(name="A", instructions="Answer the question.")],
    output="verbose",
)
result = workflow.start("What is AI?")
PresetDescription
silentNo output (default, lowest overhead)
minimalFinal answer only
statusTool calls + final output inline
traceTimestamped execution trace
verboseFull Rich panels
debugTrace + metrics

Async Execution

Diagram

main async

astart workflow 1

astart workflow 2

results

python
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.

python
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

python
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:

Diagram
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
PlaceholderResolves 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.
python
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/.

python
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.

python
# 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 otherwise

Definition 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:

python
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:

text
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):

python
result = manager.execute("research-pipeline", default_agent=agent,
                         resume="my-checkpoint", rebase_checkpoint=True)
SituationBehaviour
Resume, no checkpoint by that nameRefuses 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 editRuns 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().

python
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_over step fails on any iteration and its on_error="stop" (the Task default), the entire workflow aborts — not just the loop. result["success"] becomes False and no later steps run.
  • Completed loop steps checkpoint once. With checkpoint= set, a loop_over step persists a checkpoint after the whole loop completes (matching single-step behavior). On resume=, the loop is not re-run — execution continues from the next step.
Diagram

loop_over with checkpoint

Yes

No, on_error=stop

Step N: loop_over

Iterate items

All items OK?

💾 Save checkpoint N

🛑 Halt workflow

Step N+1

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" (the Task default) 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 final result["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.
Diagram

stop

continue

⚙️ Nested step raises

on_error?

🛑 Propagate stop → workflow aborts

▶️ Workflow keeps running

❌ status = failed

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:

markdown
---
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.


Related