Triggers
A workflow declares at least one trigger in the triggers field of its workflow.jsonc descriptor. Triggers only start runs; once running, every run behaves the same regardless of what started it.
| Kind | What fires it | trigger.kind / trigger.source inside the run |
|---|---|---|
cron | A schedule, on its expression. | cron |
webhook | Any sender pointed at an org webhook's URL. | webhook |
github / linear / jira / notion | A provider event, delivered through your org's connection. | webhook, with source set to <provider>:<event> |
manual | The Run button, boardwalk run, or another workflow via workflows.call(). | manual |
workflow_run | A named upstream workflow in the same org finishing. | manual, with an event actor |
Cron
"triggers": [
{
"kind": "cron",
"expr": "0 9 * * 1-5",
"timezone": "America/New_York",
"input": { "mode": "full" } // optional: the payload every scheduled run fires with
}
]expr takes standard 5-field cron (or 6 fields with seconds). timezone is optional and defaults to UTC; daylight saving is handled for you. A workflow may declare several cron triggers.
input is optional and pins a static payload for every run this schedule fires, passed to your run function as its first parameter. When the workflow types its input, the static payload is validated against the derived input schema at deploy, so a bad payload is rejected then, not when the schedule fires. Omit it and scheduled runs fire with no input. For a payload or cadence you compute at runtime, use workflows.schedule() from inside a run instead of a static cron trigger; it takes the same cron (or a rate or one-shot at) plus the input to fire with.
Webhook
"triggers": [
{ "kind": "webhook", "name": "stripe-prod" }
]A webhook is an endpoint your org creates once, not a property of one workflow. Create it in the dashboard (or with boardwalk webhooks create), point a sender at its URL, then attach any number of workflows by naming it here. The request body becomes each run's input, handed to run as its first parameter.
Every workflow attached to a webhook runs on every delivery. To send different events to different workflows, create a second webhook and choose which events go where on the sender's side: Stripe, Sentry, PagerDuty, GitHub and most senders let you pick events per endpoint.
The secret always travels in a header, never in the URL (URLs leak into access logs and proxies). How it is verified is a property of the webhook, chosen when you create it:
- Token: the caller sends the secret verbatim in the
X-Boardwalk-Tokenheader (or a header you name, for senders with a fixed header of their own). Simple, and right for any sender that can set a header. - Signature: the caller signs the raw request body with HMAC-SHA256 and sends it as
X-Boardwalk-Signature: sha256=<hex>. Presets match the native signing schemes of common senders (GitHub, Stripe, Slack, Linear, Sentry, PagerDuty, and anything following Standard Webhooks), so their deliveries verify without an adapter. Use signing whenever the sender supports it: the payload is cryptographically verified.
The secret is shown once when generated and regenerable at any time. Treat it like a password: anyone who has it can start a run of every workflow attached to that webhook. Naming a webhook you haven't created yet is not a deploy error: the workflow deploys and shows as not connected until it exists.
Provider triggers
Four providers have a first-class trigger: GitHub, Linear, Jira and Notion. You connect the provider once from Connections in the dashboard, and the platform verifies each delivery, dedupes it, filters it against your declared triggers, and only then creates a run: no URL, no secret, no signature code. Every delivery is logged and replayable in the inbound delivery log. The events, payload shapes, and each vendor's connection details live on Provider connections.
Manual
"triggers": [{ "kind": "manual" }]Run it on demand: the Run button in the dashboard, boardwalk run from the CLI, or another workflow via workflows.call(). Workflow-to-workflow calls are manual triggers under the hood; the parent run is recorded on the child.
On another workflow
"triggers": [
{ "kind": "workflow_run", "workflows": ["ci"], "conclusions": ["success"] }
]A workflow_run trigger fires this workflow whenever a named upstream workflow in the same org finishes. List one or more upstream slugs in workflows, and optionally filter by outcome with conclusions (success, failure, or cancelled; omit it to fire on any). The upstream run's result becomes this run's input, so you can chain workflows without wiring a webhook between them. Available on the hosted platform and a self-hosted server.
Reading the trigger payload
export default async function run(input, context) {
// input: the webhook body, run --input, workflows.call() input,
// or a cron trigger's declared input.
console.log(input);
console.log(context.trigger.kind); // "cron" | "webhook" | "manual"
}Whatever started the run, its payload arrives as run's first parameter. Type it (run(input: Payment)) and the derived schema powers a typed input form in the dashboard and describes the shape to callers; leave it bare and the raw JSON is handed to you. The second parameter, context, says how the run started: context.trigger is the transport (cron, webhook, or manual) and context.actor is the initiator (a user, a parent workflow, and so on). From the CLI, pass a payload with boardwalk deploy . --run --input '{...}'.
A webhook run also carries the delivery as the HTTP request it arrived as, so you can read whatever your sender puts outside the body:
const event = context.trigger.request?.headers["x-github-event"]; // GitHub
const topic = context.trigger.request?.headers["x-shopify-topic"]; // Shopify
const kind = input.type; // Stripe: it is in the body
if (event === "ping") return { ok: true }; // GitHub's install handshakerequest is { method, path, query, headers }, present on webhook runs only. Header names are lower-cased, since HTTP treats them case-insensitively. Boardwalk takes no view on which of them matter: senders disagree about where they name their events (a header, the body, sometimes the query string), and a platform that curated one field per sender would need a release per sender.
Whatever authenticated the delivery is removed before your program sees it: the verification header for your endpoint's scheme, plus authorization, cookie and proxy-authorization. You never receive the credential your sender used to reach you.
The kind enum stays at those three on purpose: it names the transport, and never grows to restate what actor already says:
| What started the run | trigger.kind | Distinguished by |
|---|---|---|
| A provider trigger | webhook | trigger.source is <provider>:<event> (github:pr.merged, jira:issue.created) |
| A workflow_run subscription | manual | an event actor |
A workflows.call() child | manual | a workflow actor |
Read input.event rather than trigger.kind to branch inside a provider-triggered workflow.
A run can also name the environment it executes in, which selects its secrets and variables; see Selecting an environment.