Stevora

Task Step

Execute custom business logic

The task step executes custom business logic you define in a handler function. It is the most flexible step type -- register any async function and Stevora will invoke it durably with automatic retries and state checkpointing.

Schema

FieldTypeRequiredDescription
type"task"YesStep type discriminator
namestring (1-100 chars)YesUnique step name within the workflow
handlerstringYesName of the registered handler function
configRecord<string, unknown>NoArbitrary configuration passed to the handler
retryRetryPolicyNoRetry policy for failed executions

RetryPolicy

FieldTypeDefaultConstraintsDescription
maxAttemptsnumber31 -- 20Maximum number of execution attempts
backoffMsnumber1000>= 100Initial backoff delay in milliseconds
backoffMultipliernumber21 -- 10Multiplier applied to backoff each retry

Configuration Example

{
  "type": "task",
  "name": "enrich-lead",
  "handler": "enrichLeadData",
  "config": {
    "provider": "clearbit",
    "fields": ["company", "title", "linkedin"]
  },
  "retry": {
    "maxAttempts": 5,
    "backoffMs": 2000,
    "backoffMultiplier": 3
  }
}

Handler Registration

Register handlers at application startup using registerTaskHandler. The name you pass must match the handler field in your step definition.

import { registerTaskHandler } from 'stevora/step-handlers';

registerTaskHandler('enrichLeadData', async (ctx, step) => {
  const leadId = ctx.workflowInput.leadId;
  const config = step.config as { provider: string; fields: string[] };

  const data = await enrichmentApi.enrich(leadId, config);

  return {
    status: 'completed',
    output: { enrichedData: data },
    stateUpdates: { enrichedLead: data },
  };
});

StepContext

Every handler receives a StepContext object with the current execution context:

interface StepContext {
  workflowRunId: string;                    // ID of the running workflow
  workspaceId: string;                      // Workspace the run belongs to
  stepName: string;                         // Name of this step
  workflowState: Record<string, unknown>;   // Accumulated state from prior steps
  workflowInput: Record<string, unknown>;   // Input provided when the run was created
}
  • workflowState contains the merged stateUpdates from all previously completed steps. Use it to read outputs from earlier steps in the workflow.
  • workflowInput is the input passed to POST /v1/workflow-runs when the run was created. It is immutable throughout the run.

StepResult

Handlers return a StepResult to indicate the outcome:

interface StepResult {
  status: 'completed' | 'failed' | 'waiting';
  output?: Record<string, unknown>;
  stateUpdates?: Record<string, unknown>;
  error?: { message: string; code?: string; details?: unknown };
  waitUntil?: Date;
  signalType?: string;
}
FieldDescription
status"completed" advances to the next step. "failed" triggers retry logic. "waiting" pauses the workflow.
outputPersisted on the step run record. Visible via the API.
stateUpdatesMerged into workflowState so subsequent steps can read the data.
errorRequired when status is "failed". Logged and stored on the step run.

Returning completed

return {
  status: 'completed',
  output: { score: 87 },
  stateUpdates: { leadScore: 87 },
};

Returning failed

return {
  status: 'failed',
  error: { message: 'API rate limited', code: 'RATE_LIMIT' },
};

When a handler returns failed (or throws an exception), Stevora checks the retry policy. If attempts remain, the step is re-enqueued with exponential backoff. If all attempts are exhausted, the entire workflow transitions to FAILED.

How It Works

  1. The execution engine calls dispatchStep, which looks up the handler by name via getTaskHandler.
  2. If no handler is registered for the given name, the step fails immediately with error code HANDLER_NOT_FOUND.
  3. The handler runs with the current StepContext. Any uncaught exception is caught and converted to a failed result.
  4. On success, output is persisted to the step run and stateUpdates are merged into the workflow state.
  5. The engine then advances to the next step in definition order (unless a condition step has set a __nextStep override).