Workflows
A Workflow is a live, stateful instance of a Blueprint. Where a blueprint is the design — generic and immutable — a workflow is one specific execution: real entity data, real participants, real signing events tracked end-to-end.
A workflow is:
- Transactional — represents one specific transaction (onboarding Jane Doe, closing the Acme deal)
- Stateful — tracks status (
Draft,InProgress,Completed,Failed,Voided) and per-step progress - Auditable — immutable event log + versioned entity-data history, every change linked to the step that caused it
Creating a Workflow
A workflow can run against a blueprint published in your namespace, or against one in the Library — pick the source that matches where the blueprint lives:
| Source | Field(s) on the request body | Use when |
|---|---|---|
| Published in your namespace | blueprintKey + blueprintVersion |
Standard production path — the blueprint is already pushed and live |
| From the Library | listingKey + listingVersion |
Running directly against a public or org-shared listing |
In either case, you also pass data — an object keyed by the blueprint's input keys with the entity payloads.
curl -X POST https://api.signstack.ai/v1/orgs/{orgId}/namespaces/{namespaceKey}/workflows \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"blueprintKey": "employee_onboarding",
"blueprintVersion": "1.0.0",
"data": {
"employee": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com" },
"company": { "name": "Acme Corp", "hrManager": { "name": "Alice", "email": "alice@acme.com" } }
}
}'
For one-off runs (testing, demos, ops kicking off a workflow by hand), Studio's Run action and the CLI's signstack run command both call this same endpoint — useful when you don't want to wire it into your application yet.
Run vs. Review Mode
The create-workflow request takes an optional options.mode that controls whether the workflow actually starts running:
| Mode | Behavior |
|---|---|
Run |
Default. The workflow starts executing — first step kicks off, signing emails go out, webhooks fire as events occur. |
Review |
The workflow is instantiated but not started. Participants are resolved, envelopes are rendered, but no notifications are sent. |
Review mode is useful when you want a final sanity check before going live: render the actual documents the participants will see, with the actual data, against the actual blueprint version — without committing to the live signing flow.
A workflow created in Review stays paused until you act on it. Inspect the rendered envelopes and resolved participants, apply any in-flight modifications (see below), then start it to transition into Run. Or void it and start over.
In-Flight Modifications
When a workflow is created in Review state, a few things can still change before you start it:
- Entity data —
PATCH /workflows/{id}/entitiesupdates entity values. Downstream steps see the new data. - Workflow shape —
PUT /workflows/{id}accepts updatedenvelopes,participants, orrootStep. Use this to swap a participant's email before sending, or fix a typo in a custom field, without re-creating the workflow.
These editing surfaces are scoped to the workflow itself — they don't ripple back to the blueprint. The blueprint stays immutable; the workflow is your one-off tweakable copy.
The workflow editor lets you do all of this visually — use it standalone in Studio or embed it in your own app via the <signstack-workflow> component. The component renders the editor surface automatically for pending workflows when the embed token's API key holds workflow:update.
Participants Resolved from Data
You don't pass participants directly into the create-workflow request. The blueprint declares participants with name/email expressions ($.company.representative.email), and the engine resolves them from the entity data you provided in data. Send the data, get the participants — no separate participant payload.
For testing convenience, options.overrideAllParticipantEmails routes every signing email to a single test address — saves you from having to swap real emails for test ones in the entity data.
Sending Action Links Yourself
By default SignStack emails each participant their signing link when their turn arrives. If you'd rather deliver those links through your own channel (your branding, your ESP, SMS, in-app, etc.), do two things:
- Suppress SignStack's emails — create the workflow with
options.disableParticipantNotifications: true. The engine still advances steps and fires webhooks; it just doesn't send participant emails. - Fetch the links — call
GET /workflows/{id}/action-urls. It returns one entry per participant with theirparticipantKey,name,email,stepKey,actionType(currently alwayssign),status, and theurlto send them.
{
"actionUrls": [
{ "participantKey": "buyer", "email": "avery@example.com", "stepKey": "buyer-signs", "actionType": "sign", "status": "ready", "url": "https://signstack.ai/sign?workflowId=wf_…&signerId=…" },
{ "participantKey": "seller", "email": "sam@example.com", "stepKey": "seller-signs", "actionType": "sign", "status": "pending", "url": "https://signstack.ai/sign?workflowId=wf_…&signerId=…" }
]
}
Send a link when its status is ready. ready means the participant's step is active and SignStack did not email them — it's their turn and the link is yours to deliver. pending means an earlier step hasn't finished yet; the link is returned (and stays valid) but shouldn't be sent until that participant's turn arrives. For sequential flows, re-fetch — or watch the participant.task_assigned webhook — to learn when later participants become ready. (With notifications left enabled, an active participant shows notified instead, since SignStack already emailed them.)
Treat these URLs as credentials. Each
urlembeds a token that is the only thing required to complete the action on that participant's behalf — there is no separate login. Send each link only to its intended recipient over a trusted channel, and never log or expose them client-side. Because of this, the endpoint requires the dedicatedworkflow:action-urlsscope — it is not granted byworkflow:read. See Scopes & Permissions.
Redirecting Signers After a Step
By default, a participant who finishes signing lands on SignStack's completion screen. To send them somewhere of your own instead — an order-status page, a thank-you page, or the next task in your product — configure per-step completion redirects under options when you create the workflow:
{
"blueprintKey": "offer_letter",
"blueprintVersion": "1.2.0",
"data": {
/* … */
},
"options": {
"stepRedirects": [
{ "stepKey": "employee_signs", "redirectUrl": "https://app.example.com/onboarding/next" },
{ "stepKey": "manager_signs", "redirectUrl": "https://app.example.com/hr/dashboard" }
]
}
}
Each entry attaches a redirect to one step. The moment that step's participant finishes, they're sent to your redirectUrl instead of the default screen.
SignStack appends the step-completion context as signed query params, mirroring the step.completed webhook so the page you land on gets the same signal whether it listens on webhooks or reads the URL:
https://app.example.com/onboarding/next?workflowId=wf_…&stepKey=employee_signs&eventType=step.completed×tamp=1700000000000&signature=a31618…
Any query params already on your configured URL are preserved.
Verifying the redirect signature
The redirect lands on a public URL, so anyone could craft those params and call your page directly. To stop that, SignStack signs the redirect with the same HMAC-SHA256 primitive it uses for webhooks — hex digest, timestamp. prefix, a 5-minute replay window, and comma-separated candidates during secret rotation. Verify it on your backend before trusting a redirect.
The redirect is not byte-for-byte a webhook, so don't pass it to your webhook verifier: the signed message is the query params (workflowId=…&stepKey=…&eventType=…), not a JSON body, and the two values ride in plain timestamp and signature query params rather than a single t=…,v1=… header. Use the dedicated check below.
signatureis anHMAC-SHA256digest (hex) of the string`${timestamp}.workflowId=${workflowId}&stepKey=${stepKey}&eventType=${eventType}`— the signed params in that fixed order, prefixed withtimestampand a.separator.- It is keyed by your webhook signing secret (the secret issued when you create a webhook endpoint). The redirect is signed only when your namespace has a webhook endpoint; with none configured,
timestampandsignatureare omitted. timestampis Unix milliseconds. Reject anything older than 5 minutes to prevent a captured URL from being replayed.- During a secret rotation — or if you have multiple endpoints —
signaturemay be a comma-separated list; a match against any value is valid.
Validate it on your server (never in the browser, where the attacker controls the page):
const crypto = require('crypto');
function verifyRedirect(query, webhookSigningSecret) {
if (!query || typeof query !== 'object') return false;
const { workflowId, stepKey, eventType, timestamp, signature } = query;
// Reject anything that isn't a plain string (e.g. duplicated query params
// arrive as arrays in Express and would throw on `.split`).
if (
typeof workflowId !== 'string' ||
typeof stepKey !== 'string' ||
typeof eventType !== 'string' ||
typeof timestamp !== 'string' ||
typeof signature !== 'string'
) {
return false;
}
// Replay window: 5 minutes, same as webhooks. Guard NaN — a non-numeric
// timestamp must fail closed, not slip past the window check.
const ts = Number(timestamp);
if (Number.isNaN(ts) || Math.abs(Date.now() - ts) > 5 * 60 * 1000) {
return false;
}
const canonical =
`${timestamp}.workflowId=${workflowId}&stepKey=${stepKey}&eventType=${eventType}`;
const expected = crypto
.createHmac('sha256', webhookSigningSecret)
.update(canonical)
.digest('hex');
// `signature` may hold several comma-separated candidates — accept any match.
return signature.split(',').some((candidate) => {
const a = Buffer.from(candidate);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}
Only treat the params as genuine once the signature verifies. For irreversible actions, still confirm the authoritative state via webhooks or GET /workflows/{id}.
Rules, enforced at create time (violations return 400):
stepKeymust be a participant step that exists in the blueprint's orchestration. Group steps are rejected — a group has no single participant to redirect.- Each
stepKeymay appear at most once. redirectUrlmust be an absolutehttp(s)URL (protocol required), up to 2048 characters.
This applies to SignStack-hosted signing only. When SignStack hosts the signing experience, the completion screen shows a short "Redirecting in 3, 2, 1…" countdown and then sends the browser to your URL (only http(s) URLs are ever navigated to).
Embedded signing does not redirect. When you embed the signing component (
<signstack-participant>),stepRedirectsis not applied — the component stays in place and does not navigate your host page. Drive post-signing navigation yourself by listening for the component'ssignedevent and routing within your own app.
Lifecycle and Monitoring
A workflow moves through these states:
Draft → InProgress → Completed
↘ → Failed
↘ → Voided
- Draft — Created but not started (typical for
Reviewmode or before the first step is processed). - InProgress — Engine is executing steps. Participants have been notified or signing actions are in flight.
- Completed — Every required step has finished successfully.
- Failed — A step's
validateexpression rejected, or a hard error occurred. - Voided — Cancelled. Either explicitly via
POST /workflows/{id}/void, or when a signer declines through the signing flow.
To watch progress, poll GET /workflows/{id} or — preferred — subscribe via webhooks for push notifications as events happen.
Related Concepts
- Blueprints — The design every workflow runs from
- The Workflow Engine (Steps) — Deep dive on how steps execute, branch, and update data
- Library — Running workflows directly from listings via
listingKey/listingVersion - Webhooks — Real-time event notifications
