Stevora

Workflows & Steps

Understanding the workflow lifecycle and step execution model

Workflows & Steps

A workflow in Stevora is a sequence of steps executed durably. Each run is persisted to PostgreSQL, so if a process crashes mid-execution, the workflow resumes exactly where it left off.

Workflow Lifecycle

Every workflow run moves through a finite set of statuses. The engine enforces these transitions — invalid transitions are rejected.

                    ┌────────────────────────────────────┐
                    │                                    v
  PENDING ──> RUNNING ──> WAITING ──> RUNNING ──> COMPLETED
     │           │           │           │
     │           │           │           └──> FAILED
     │           │           │                  │
     │           │           └──> CANCELLED     └──> RUNNING (retry)
     │           │                                      │
     │           └──> PAUSED ──> RUNNING                │
     │           │                                      │
     │           └──> FAILED ──────────────────────────-┘
     │           │
     │           └──> CANCELLED

     └──> CANCELLED

The valid transitions are defined explicitly in the state machine:

const workflowTransitions: Record<WorkflowRunStatus, WorkflowRunStatus[]> = {
  PENDING:   ['RUNNING', 'CANCELLED'],
  RUNNING:   ['WAITING', 'PAUSED', 'COMPLETED', 'FAILED', 'CANCELLED'],
  WAITING:   ['RUNNING', 'CANCELLED', 'FAILED'],
  PAUSED:    ['RUNNING', 'CANCELLED'],
  COMPLETED: [],
  FAILED:    ['RUNNING'], // allow retry from failed
  CANCELLED: [],
};

Terminal states are COMPLETED, FAILED, and CANCELLED. Once a workflow reaches a terminal state, the engine will not process it further (unless explicitly retried from FAILED).

Step Lifecycle

Steps follow a similar state machine:

const stepTransitions: Record<StepRunStatus, StepRunStatus[]> = {
  PENDING:   ['RUNNING', 'SKIPPED', 'CANCELLED'],
  RUNNING:   ['WAITING', 'COMPLETED', 'FAILED', 'CANCELLED'],
  WAITING:   ['RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED'],
  COMPLETED: [],
  FAILED:    ['RUNNING'], // allow retry
  SKIPPED:   [],
  CANCELLED: [],
};

When a step completes, the engine automatically advances to the next step in the definition. When the last step completes, the workflow transitions to COMPLETED.

The 6 Step Types

Stevora provides six step types that serve as building blocks for any workflow.

Task

A general-purpose step that runs a registered handler function. Use this for custom business logic, API calls, data transformations, or anything that does not fit the other types.

{
  "type": "task",
  "name": "enrich-lead",
  "handler": "enrichLeadData",
  "config": { "source": "clearbit" }
}

Wait

Pauses the workflow for a duration or until a specific timestamp. The engine schedules a delayed job and resumes automatically.

{
  "type": "wait",
  "name": "cooldown-period",
  "durationMs": 86400000
}

You can also wait until an absolute time:

{
  "type": "wait",
  "name": "wait-until-market-open",
  "untilTimestamp": "2025-01-06T14:30:00Z"
}

Condition

Evaluates an expression against the current workflow state and branches to different steps based on the result.

{
  "type": "condition",
  "name": "check-qualification",
  "expression": "leadScore",
  "trueStep": "send-premium-offer",
  "falseStep": "send-nurture-email"
}

The expression is evaluated by walking the workflow state object. If the resolved value is truthy, execution jumps to trueStep; otherwise it jumps to falseStep.

External Event

Pauses the workflow and waits for an external signal (webhook callback, user action, third-party event). Optionally includes a timeout.

{
  "type": "external_event",
  "name": "wait-for-payment",
  "signalType": "payment.received",
  "timeoutMs": 3600000
}

Resume the workflow by sending a signal through the API:

POST /api/v1/workflow-runs/:id/resume
{ "signalType": "payment.received", "payload": { "amount": 99.00 } }

LLM

Calls a language model with first-class support for tool calling, model fallback, guardrails, and structured output.

{
  "type": "llm",
  "name": "draft-email",
  "model": "gpt-4o",
  "fallbackModels": ["claude-sonnet-4-20250514"],
  "systemPrompt": "You are a sales assistant.",
  "messages": [
    { "role": "user", "content": "Draft an outreach email for {{state.leadName}}." }
  ],
  "responseFormat": "json",
  "temperature": 0.7,
  "maxToolRounds": 5
}

Approval

Pauses the workflow for human review. A reviewer can approve, reject, or edit the content before execution continues.

{
  "type": "approval",
  "name": "review-email",
  "contentKey": "draftEmail",
  "prompt": "Review this email before sending.",
  "timeoutMs": 86400000
}

Step Execution Flow

When the engine picks up a workflow run from the queue, it follows this sequence:

  1. Fetch the workflow run with its definition and existing step runs.
  2. Guard against terminal states — skip if already COMPLETED, FAILED, or CANCELLED.
  3. Resolve the next step — either an explicit target, the __nextStep set by a condition, or the next step in definition order.
  4. Idempotency check — if the step is already COMPLETED or SKIPPED, advance immediately.
  5. Transition the workflow and step to RUNNING (with optimistic locking).
  6. Dispatch to the appropriate step handler based on the step type.
  7. Handle the result:
    • completed — persist output and state updates, enqueue the next step.
    • waiting — transition to WAITING, schedule a delayed resume job if applicable.
    • failed — evaluate the retry policy. If retries remain, schedule a retry. Otherwise, fail the workflow.

Defining a Workflow

A workflow definition is a named, versioned list of steps validated with Zod:

const workflowDefinitionInputSchema = z.object({
  name: z.string().min(1).max(200),
  version: z.string().min(1).max(50),
  description: z.string().max(1000).optional(),
  steps: z.array(stepDefinitionSchema).min(1),
});

Every step inherits a base schema with a name and optional retry policy, then extends it with type-specific fields.