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
| Field | Type | Required | Description |
|---|---|---|---|
type | "task" | Yes | Step type discriminator |
name | string (1-100 chars) | Yes | Unique step name within the workflow |
handler | string | Yes | Name of the registered handler function |
config | Record<string, unknown> | No | Arbitrary configuration passed to the handler |
retry | RetryPolicy | No | Retry policy for failed executions |
RetryPolicy
| Field | Type | Default | Constraints | Description |
|---|---|---|---|---|
maxAttempts | number | 3 | 1 -- 20 | Maximum number of execution attempts |
backoffMs | number | 1000 | >= 100 | Initial backoff delay in milliseconds |
backoffMultiplier | number | 2 | 1 -- 10 | Multiplier 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
}workflowStatecontains the mergedstateUpdatesfrom all previously completed steps. Use it to read outputs from earlier steps in the workflow.workflowInputis the input passed toPOST /v1/workflow-runswhen 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;
}| Field | Description |
|---|---|
status | "completed" advances to the next step. "failed" triggers retry logic. "waiting" pauses the workflow. |
output | Persisted on the step run record. Visible via the API. |
stateUpdates | Merged into workflowState so subsequent steps can read the data. |
error | Required 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
- The execution engine calls
dispatchStep, which looks up the handler by name viagetTaskHandler. - If no handler is registered for the given name, the step fails immediately with error code
HANDLER_NOT_FOUND. - The handler runs with the current
StepContext. Any uncaught exception is caught and converted to afailedresult. - On success,
outputis persisted to the step run andstateUpdatesare merged into the workflow state. - The engine then advances to the next step in definition order (unless a condition step has set a
__nextStepoverride).