foliot docs

API reference / Webhook / Foliot pushes a task to your handler

Foliot pushes a task to your handler

POSTSent by Foliot to your handler

No authentication.

Foliot’s only outbound call. It is a command, not a notification: it tells your application to execute one step, and the run is blocked until you answer. Your handler’s latency is the run’s latency.

It fires once per task-step attempt — so again on every retry, and again after a rewind or redrive.

Where it arrives

POST {handlerBaseUrl}/foliot/{workflowName}/{step}

handlerBaseUrl is set per app. A step can override the path with its own handler field.

Route on the body, not the path. The payload carries workflowName and step; a step that overrides handler will not have a path matching them.

Verifying it

foliot-signature: t=1759036800,v1=<hex hmac-sha256>

The signed string is ${t}.${rawBody}, keyed by the app’s signing secret, HMAC-SHA256, hex — the same construction as Stripe’s.

Verify over the raw bytes. Parsing the JSON and re-serialising it changes the bytes, and the signature will not match. Read the body as a string first.

Reject a timestamp more than 300 seconds from now, and compare in constant time.

The header can carry more than one v1=. During a signing-secret rollover Foliot signs with both the new and the outgoing secret, so a handler that has not picked up the new one yet still verifies. Accept the request if any signature matches a secret you hold, and do not assume there is exactly one.

The other headers — foliot-deadline, foliot-task-token, foliot-app, traceparent — mirror fields in the body and exist for logging and tracing convenience. The body is authoritative.

Answering

You returnMeaning
200 {"output": …}Step done. output becomes state[step] and the run advances.
200 with an empty bodyStep done, no output. Equivalent to {"output": null}.
200 {"output": …, "next": "refund"}Step done, and you choose where the run goes. Must name a declared step or a sentinel ($end, $fail, $stuck).
202Accepted; you will finish later via POST /apps/{appId}/tasks/{token}/complete or /fail. The run parks until you do, or until deadline passes.
4xx / 5xx with {"error": {...}}Failed. The retry policy decides what happens next.

On failure, error.retryable decides whether another attempt happens, and error.class is what a retry policy’s retryOn matches against. Without them Foliot classifies by status: 408 and 429 are retryable, other 4xx are not, 5xx are.

Delivered more than once

Deduplicate on idempotencyKey. It is {runId}:{generation}:{step}:{attempt} and is stable across redeliveries of the same attempt. The timestamp window is replay protection, not delivery-once — a network failure after you committed but before Foliot read your response produces a second delivery carrying the same key.

Limits

A step’s output is capped at 256 KB (FOLIOT_PAYLOAD_MAX_BYTES), measured as the JSON Foliot stores. Over it fails the step with error class output_too_large, naming the step, and the run goes to the step’s onFailure if it has one (the handler’s work happened, so a compensation should run) and fails otherwise. It is not retried, because the same handler would return the same thing. The cap is per step: the run’s accumulated state, which is sent back to every later step, is not capped by steps (editRunState holds each value of its patch to the same limit, measured the same way, not the resulting state), so for large data return a reference (a storage key or a URL) and not the data. Completing a task out of band (completeTask) with an output over the limit, or a next naming no step, fails the step the same way and answers 400, so the error shows up where the bad body was sent. Sizes are the compact JSON, not escaped, so <, > and & count as one byte each and U+2028 and U+2029 as three. A run’s input and a signal payload are held to the same limit. A synchronous attempt is additionally capped by FOLIOT_SYNC_TASK_MAX_MS; work that runs longer should answer 202 and complete out of band.

Headers

foliot-signature
string
REQUIRED

t=<unix-seconds>,v1=<hex HMAC-SHA256 of ${t}.${rawBody}>, with one v1= per signing secret: two during a rollover.

pattern ^t=\d+(,v1=[0-9a-f]{64})+$

foliot-deadline
string (date-time)
REQUIRED

Mirrors deadline in the body. After it, the attempt is abandoned.

foliot-task-token
string (uuid)
REQUIRED

Mirrors taskToken in the body.

foliot-app
string
REQUIRED

Mirrors appId in the body.

traceparent
string
REQUIRED

W3C traceparent whose trace id is the run id.

Payload

appId
string
REQUIRED
attempt
integer (int64)
REQUIRED
businessKey
string,null
REQUIRED
deadline
string (date-time)
REQUIRED

After this the attempt is abandoned as timed out.

generation
integer (int64)
REQUIRED
idempotencyKey
string
REQUIRED

{runId}:{generation}:{step}:{attempt}.

input
any
REQUIRED

The run's input.

runId
string (uuid)
REQUIRED
state
map of any
REQUIRED

Earlier steps' outputs, keyed by step name.

step
string
REQUIRED
taskToken
string (uuid)
REQUIRED

Complete or fail the task with this.

traceparent
string
REQUIRED

W3C traceparent whose trace id is the run id.

workflowName
string
REQUIRED
workflowVersion
integer (int64)
REQUIRED

Responses

200 object The step is done. An empty body is accepted and treated as completion with no output. next overrides the step the run moves to and must name a declared step or a sentinel ($end, $fail, $stuck).
202 no body Accepted, finishing later. The run parks until you call the task complete or fail route with taskToken, or the deadline passes and the attempt times out.
4XX object Failed. 408 and 429 are treated as retryable, other 4xx as not; a retryable field in the body overrides that.
5XX object Failed, retryable by default.