Skip to main content
Steps are the fundamental unit of durability in Polos. Each step’s execution is persisted, enabling workflows to resume from the last completed step after failures.

What is a step?

A step is a durable operation within a workflow. When a step completes, its output is saved to the database. If the workflow crashes, completed steps return their cached results on replay - no re-execution, no duplicate side effects.
On failure after step 2:

Step execution model

Unlike workflows, which are scheduled by the orchestrator and assigned to workers, steps execute directly on the same worker running the workflow. This means:
  • Lower overhead - No scheduling delay
  • Same execution context - Access to workflow variables
  • Durability guaranteed - Output is persisted before continuing
Steps are not queued or orchestrated — they run inline with the workflow code.

Core step methods

step.run()

Run any Python function as a durable step:
With retry configuration:
What should be a step:
  • ✅ External API calls
  • ✅ Database operations
  • ✅ File I/O
  • ✅ Non-deterministic operations (random(), datetime.now())
  • ✅ Any operation that might fail
What should NOT be a step:
  • ❌ Pure logic (if statements, loops)
  • ❌ Variable assignments
  • ❌ String manipulation

step.invoke()

Start a child workflow without waiting for it to complete:

step.invoke_and_wait()

Start a child workflow and suspend until it completes:
Worker behavior: The worker suspends the parent workflow (no compute consumed) until the child completes.

step.batch_invoke()

Start multiple workflows in parallel without waiting:

step.batch_invoke_and_wait()

Start multiple workflows in parallel and wait for all to complete:

step.agent_invoke()

Start an agent execution without waiting for it to complete:

step.agent_invoke_and_wait()

Start an agent execution and suspend until it completes:
Worker behavior: The worker suspends the parent workflow (no compute consumed) until the agent completes.

step.batch_agent_invoke()

Start multiple agent executions in parallel without waiting:

step.batch_agent_invoke_and_wait()

Start multiple agent executions in parallel and wait for all to complete:

step.wait_for()

Pause execution for a duration:
Available units: seconds, minutes, hours, days, weeks

step.wait_until()

Wait until a specific datetime:

step.wait_for_event()

Wait for an external event:

step.suspend()

Suspend execution and wait for manual resume. This is syntactic sugar around wait_for_event that’s tailored for agent human-in-the-loop flows.
See Human-in-the-Loop for details.

step.publish_event()

Publish events durably:

step.uuid()

Generate a UUID that persists across replays:
Why this matters:

step.now()

Get current timestamp that persists across replays:

step.trace()

Add custom spans for observability:
See Tracing for details.

Step keys must be unique

Each step needs a unique step_key per execution:

Agents and steps

In Polos, agents are special workflows. Everything an agent does is broken into steps: Agent actions as steps:
  • LLM calls - Each call is a step
  • Guardrail evaluation - Each guardrail is a step
  • Stop condition checks - Each check is a step
  • Tool calls - Tools are subworkflows (not steps)
This means:
  • Agent failures resume from the last completed step (not the beginning)
  • Tool calls are durable subworkflows (not lost on failure)
  • Every agent action is observable via step events