Stevora

External Event Step

Wait for external signals to resume workflow

The external event step pauses the workflow and waits for an external system to send a signal. This is how you integrate webhooks from third-party services, user actions, or any asynchronous event into a durable workflow.

Schema

FieldTypeRequiredDescription
type"external_event"YesStep type discriminator
namestring (1-100 chars)YesUnique step name within the workflow
signalTypestringYesThe signal name this step is waiting for
timeoutMsnumberNoMaximum time to wait in milliseconds before failing
retryRetryPolicyNoRetry policy for failed executions

Configuration Example

{
  "type": "external_event",
  "name": "wait-for-payment",
  "signalType": "payment_confirmed",
  "timeoutMs": 86400000
}

This step pauses the workflow and waits up to 24 hours for a payment_confirmed signal.

How It Works

Step execution

  1. When the engine reaches an external event step, it calls executeExternalEventStep.

  2. The handler returns a waiting result with the signalType and an optional waitUntil timeout:

    return {
      status: 'waiting',
      signalType: step.signalType,
      ...(step.timeoutMs && {
        waitUntil: new Date(Date.now() + step.timeoutMs),
      }),
    };
  3. The engine transitions the step to WAITING and the workflow to WAITING.

  4. A WORKFLOW_PAUSED event is recorded with reason: "external_event" and the signalType.

  5. If timeoutMs is set, a delayed BullMQ job is also scheduled as a timeout guard.

Resuming with a signal

To resume the workflow, send a signal via the resume API endpoint:

POST /v1/workflow-runs/{id}/resume

Request body:

{
  "signalType": "payment_confirmed",
  "payload": {
    "transactionId": "txn_abc123",
    "amount": 99.00,
    "currency": "USD"
  }
}

The signalType in the request must match the signalType the step is waiting for. The payload is merged into the workflow state so subsequent steps can access the signal data.

Response:

{
  "success": true,
  "data": {
    "id": "run_xyz",
    "status": "RUNNING",
    "currentStepName": "wait-for-payment"
  }
}

Resume schema

FieldTypeRequiredDescription
signalTypestringYesMust match the step's signalType
payloadRecord<string, unknown>NoData to merge into workflow state

Timeout Behavior

When timeoutMs is set, the engine schedules a delayed BullMQ job alongside the wait. If the timeout elapses before a signal arrives, the workflow handles it according to the retry policy:

  • If retries remain, the step is retried (and enters the WAITING state again).
  • If all retries are exhausted, the workflow transitions to FAILED.

If a signal arrives before the timeout, the delayed timeout job is effectively a no-op -- the engine sees the step is no longer in WAITING status and skips processing.

Practical Usage

External event steps are useful for:

  • Payment confirmation -- pause until a payment gateway sends a webhook
  • Third-party callbacks -- wait for a background check, credit report, or API result
  • User actions -- pause until a user clicks a link, fills out a form, or makes a choice
  • Cross-system orchestration -- coordinate between microservices with signal-based handoffs

Example: order fulfillment with external events

{
  "name": "order-fulfillment",
  "version": "1.0",
  "steps": [
    {
      "type": "task",
      "name": "create-order",
      "handler": "createOrder"
    },
    {
      "type": "external_event",
      "name": "wait-for-payment",
      "signalType": "payment_confirmed",
      "timeoutMs": 86400000
    },
    {
      "type": "task",
      "name": "fulfill-order",
      "handler": "fulfillOrder"
    },
    {
      "type": "external_event",
      "name": "wait-for-shipping",
      "signalType": "shipment_delivered",
      "timeoutMs": 604800000
    },
    {
      "type": "task",
      "name": "send-review-request",
      "handler": "requestReview"
    }
  ]
}

In this workflow, the fulfill-order step only runs after payment is confirmed, and the review request only goes out after the shipment is delivered. Each external event step resumes when the matching signal arrives via the API.