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 dataPATCH /workflows/{id}/entities updates entity values. Downstream steps see the new data.
  • Workflow shapePUT /workflows/{id} accepts updated envelopes, participants, or rootStep. 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.

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:

  1. 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.
  2. Fetch the links — call GET /workflows/{id}/action-urls. It returns one entry per participant with their participantKey, name, email, stepKey, actionType (currently always sign), status, and the url to 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 url embeds 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 dedicated workflow:action-urls scope — it is not granted by workflow: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&timestamp=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.

  • signature is an HMAC-SHA256 digest (hex) of the string `${timestamp}.workflowId=${workflowId}&stepKey=${stepKey}&eventType=${eventType}` — the signed params in that fixed order, prefixed with timestamp and 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, timestamp and signature are omitted.
  • timestamp is 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 — signature may 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):

  • stepKey must be a participant step that exists in the blueprint's orchestration. Group steps are rejected — a group has no single participant to redirect.
  • Each stepKey may appear at most once.
  • redirectUrl must be an absolute http(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>), stepRedirects is not applied — the component stays in place and does not navigate your host page. Drive post-signing navigation yourself by listening for the component's signed event 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 Review mode 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 validate expression 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.

  • 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