Skip to content

Human-in-the-Loop Workflows

Martha workflows can pause execution and wait for an external event before continuing. This enables approval gates, payment confirmations, document reviews, and any pattern where a workflow needs input from a person or external system.

This guide covers two audiences:

  • Workflow designers — how to add wait nodes to workflow definitions
  • Integrators — how to send events to waiting workflows via webhooks

For Workflow Designers

When to Use a Wait Node

Use a wait node (type: WAIT) when a workflow step needs to block until something happens outside the system:

PatternExample
Approval gateManager must approve an expense before processing
Payment confirmationWait for a payment provider callback
Document reviewHold until a reviewer marks a document as accepted
External processingWait for a third-party system to finish a job

If you only need a fixed delay (e.g., "wait 30 seconds before retrying"), a wait node without event_type acts as a simple timer — no webhook needed.

Adding a Wait Node

A wait node is a step in your workflow definition with type: WAIT and handler: temporal:wait_for_event:

json
{
  "id": "wait_for_approval",
  "type": "WAIT",
  "handler": "temporal:wait_for_event",
  "event_type": "manager_approval",
  "timeout_seconds": 3600,
  "output_key": "approval_result"
}

Fields

FieldRequiredDefaultDescription
idYesUnique step identifier within the workflow
typeYesMust be WAIT
handlerYesMust be temporal:wait_for_event
event_typeNoThe event type to wait for. Omit for a pure timer.
event_filterNo{}Match conditions on the incoming event data (AND logic)
timeout_secondsNo60Maximum time to wait before timing out
output_keyNoWorkflow context key where the event result is stored

Event Filtering

Use event_filter to only accept events whose event_data matches specific field values. All conditions use AND logic — every field must match.

json
{
  "id": "wait_for_approval",
  "type": "WAIT",
  "handler": "temporal:wait_for_event",
  "event_type": "manager_approval",
  "event_filter": {
    "approved": true
  },
  "timeout_seconds": 3600,
  "output_key": "approval_result"
}

With this filter, only events where event_data.approved is true will resolve the wait. An event with {"approved": false} is ignored and the node keeps waiting.

An empty filter ({}) or no filter at all matches any event with the correct event_type.

Accessing Event Data Downstream

When a wait node resolves, the result is stored under the output_key in the workflow context. Subsequent steps can reference it.

On signal (event received):

json
{
  "output": {
    "approved": true,
    "approver": "jane@example.com"
  },
  "event_type": "manager_approval",
  "source": "signal",
  "sender": "webhook",
  "received_at": "1707820800"
}

On timeout (no matching event within deadline):

json
{
  "output": null,
  "event_type": "manager_approval",
  "source": "timeout",
  "timed_out": true
}

Check the source field to branch on whether the event arrived or the node timed out.

Common Patterns

Approval Gate with Timeout Fallback

json
[
  {
    "id": "request_approval",
    "type": "ACTION",
    "handler": "send_approval_request",
    "output_key": "approval_request"
  },
  {
    "id": "wait_for_approval",
    "type": "WAIT",
    "handler": "temporal:wait_for_event",
    "event_type": "expense_approval",
    "event_filter": { "approved": true },
    "timeout_seconds": 86400,
    "output_key": "approval_result"
  },
  {
    "id": "check_approval",
    "type": "CONDITION",
    "condition": {
      "field": "approval_result.source",
      "operator": "equals",
      "value": "signal"
    },
    "then_step": "process_expense",
    "else_step": "notify_timeout"
  }
]

Payment Confirmation

json
{
  "id": "wait_for_payment",
  "type": "WAIT",
  "handler": "temporal:wait_for_event",
  "event_type": "payment_confirmed",
  "timeout_seconds": 1800,
  "output_key": "payment_result"
}

The payment provider sends a webhook when the transaction completes. The workflow continues with the payment details in payment_result.output.


For Integrators

To resume a waiting workflow, send an HTTP request to Martha's webhook endpoint. The request must be signed with HMAC-SHA256 for security.

Endpoint

POST /api/webhooks/{event_type}

Path parameter:

  • event_type — The event type that the wait node is listening for (e.g., manager_approval, payment_confirmed)

Required Headers

HeaderFormatDescription
X-Webhook-Signaturesha256=<hex_digest>HMAC-SHA256 signature of the raw request body
X-Webhook-TimestampUnix timestamp (seconds)Current time as integer. Must be within 5 minutes of server time.
X-Webhook-IdUnique stringIdempotency key. Duplicates within 10 minutes are rejected.
Content-Typeapplication/jsonRequired

Request Body

json
{
  "tenant_id": "tenant_123",
  "workflow_id": "wf-abc-123",
  "event_data": {
    "approved": true,
    "approver": "jane@example.com",
    "comment": "Looks good"
  }
}
FieldRequiredDescription
tenant_idYesTenant that owns the workflow
workflow_idYesTemporal workflow execution ID to signal
event_dataNoCustom payload passed to the wait node (defaults to {})

Response

202 Accepted — Signal delivered to the workflow:

json
{
  "status": "delivered",
  "workflow_id": "wf-abc-123"
}

!!! note The 202 response means the signal was sent to the workflow, not that the wait node matched. If the event doesn't match the node's event_filter, the signal is buffered but the node keeps waiting.

Computing the HMAC Signature

Sign the raw request body bytes with your tenant's webhook secret using HMAC-SHA256.

=== "Python"

```python
import hashlib
import hmac
import json
import time

import requests

secret = "your_webhook_secret"
body = json.dumps({
    "tenant_id": "tenant_123",
    "workflow_id": "wf-abc-123",
    "event_data": {"approved": True, "approver": "jane@example.com"}
})

timestamp = str(int(time.time()))
signature = hmac.new(
    secret.encode(), body.encode(), hashlib.sha256
).hexdigest()

response = requests.post(
    "https://martha.example.com/api/webhooks/manager_approval",
    headers={
        "Content-Type": "application/json",
        "X-Webhook-Signature": f"sha256={signature}",
        "X-Webhook-Timestamp": timestamp,
        "X-Webhook-Id": "evt-unique-id-001",
    },
    data=body,
)
print(response.status_code, response.json())
```

=== "curl"

```bash
SECRET="your_webhook_secret"
BODY='{"tenant_id":"tenant_123","workflow_id":"wf-abc-123","event_data":{"approved":true}}'
TIMESTAMP=$(date +%s)
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST "https://martha.example.com/api/webhooks/manager_approval" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=$SIGNATURE" \
  -H "X-Webhook-Timestamp: $TIMESTAMP" \
  -H "X-Webhook-Id: evt-$(uuidgen)" \
  -d "$BODY"
```

Querying Pending Events

To check what events a workflow is currently waiting for:

GET /api/admin/executions/{workflow_id}/pending-events

Response (200):

json
{
  "workflow_id": "wf-abc-123",
  "pending_events": [
    "manager_approval",
    "document_review"
  ]
}

An empty pending_events array means the workflow is not currently waiting for any external events.

Error Codes

StatusCauseDetail
400Missing X-Webhook-Id headerMissing webhook id
401Invalid or missing HMAC signatureInvalid signature
401Timestamp missing, invalid, or outside 5-minute windowTimestamp too old / Timestamp is in the future
404Workflow ID not found or not runningWorkflow not found: {id}
409Duplicate X-Webhook-Id (within 10-minute window)Duplicate webhook
413Request body exceeds 1 MBPayload too large (max 1MB)
422Missing required fields in request bodyInvalid request body
429More than 60 requests per minute for this tenantRate limit exceeded

Security Summary

Webhook requests pass through these checks in order:

  1. Payload validation — Valid JSON with required fields
  2. Size check — Body must be under 1 MB
  3. HMAC verification — Signature must match using the tenant's webhook secret
  4. Timestamp freshness — Must be within 5 minutes (rejects both old and future timestamps)
  5. IdempotencyX-Webhook-Id must not have been seen in the last 10 minutes
  6. Rate limiting — Max 60 requests per minute per tenant

Martha is built by aiaiai-pt.