BuildingTriggers

Triggers

A trigger decides when a Workflow fires. invisible-string supports five kinds, and every kind lands the same way: the platform authenticates the event, normalizes it, and starts a pipeline run — the Workflow's published steps interpreted in order, with the event's data addressable as @trigger.<key> from any step. Compiled agents carry no per-trigger channels; the trigger surface lives entirely in the platform.

KindWhat fires itWhat lands in the eventWhere the result goesHow it authenticates
ManualThe workflow editor's Run popoverNo structured data — your message is kept as run provenanceThe run's step timeline in the Runs tabYour authenticated session in the app
WebhookAn HTTP POST to the ingress URLThe raw JSON body, verbatim, as dataThe run's step timeline — the HTTP response is only an acknowledgementThe whk_ ingress token in the URL
FormA submission of the rendered formEach declared field, type-coerced and validatedThe run's step timelineThe same per-workflow ingress token, embedded in the form's submit URL
SlackAn @mention or DM the connected app receives{eventType, channel, ts, thread_ts, text, user, team, channelType}The run's step timeline — plus a thread reply, only when the Workflow declares oneSlack's request signature, verified within a replay window
ScheduleThe platform's cron ticker, on tickdata.scheduledForThe run's step timelineN/A — the platform fires it, nothing external calls in

One rule applies to all five: by default, a trigger firing while another run of the same Workflow is still live is skipped, not queued — protecting cursor state from a slow run overlapping the next window. A skipped webhook or form dispatch still answers 2xx, with a body saying the run was skipped.

Manual

Manual is the default trigger: fire the Workflow by hand from the workflow editor's Run popover and follow the run's step timeline in the Runs tab. It carries no structured trigger data — the steps themselves describe the whole job, and the message you type in the popover is stored on the run as provenance.

Webhook

A per-Workflow ingress token accepts an HTTP POST, and the entire JSON body becomes the trigger event's data — no schema, no coercion; every top-level key is addressable as @trigger.<key>. Tokens are minted as whk_ plus 43 URL-safe base64 characters (256 bits of entropy), shown once at mint time, and never retrievable again: the platform stores only their SHA-256 hash, and POST /t/:token hashes the presented token to look it up.

Before any JSON parsing happens, the ingress caps a request body at 256 KiB and rejects anything larger with payload_too_large, then checks per-token and per-IP rate limits, both returning Retry-After when exceeded — see Limits and defaults for the exact numbers. The HTTP response to that POST only acknowledges that a run started (it carries the run's id) or that the dispatch was skipped for overlap — it is never the result. Read the actual outcome in the Workflow's Runs tab; see Build a webhook Workflow for a worked example.

Form

A bound field schema renders a submittable form, and each field is one of text, textarea (Long text), number, select, checkbox, or date. Every field has a key — which must start with a letter — plus a label and a required flag, with optional placeholder/helpText; a select field additionally carries its options list, which every other type disallows.

Submitting the form runs through the same POST /t/:token ingress as Webhook, just with the token embedded in the form's own submit URL rather than typed by hand. Each value is coerced to its declared type — numbers parsed, checkboxes normalized to booleans — and a submission is rejected outright, rather than passed through malformed, if it's missing a required field or a select value falls outside its declared options. Unknown submitted keys are silently dropped, since the form schema is authoritative. Duplicate field keys are caught earlier, when the Workflow itself is saved.

Slack

Slack events route through the connected platform Slack app. Every incoming request is verified by signature within a five-minute replay window before anything else happens, and the workspace and Workflow are resolved from the team id and the trigger's binding — see Connect Slack for how that connection is set up.

The bot only receives events from channels it has been invited to — inviting it to each channel that should fire the trigger isn't optional. Every Slack event that passes the binding starts a new pipeline run. Two toggles on the binding decide what passes: mentionOnly (default true) means only @mentions start a run in a fresh thread — but a reply in a thread where an agent step already holds a session dispatches regardless, since a reply is part of a conversation already underway; and includeDirectMessages (default false) lets DMs start runs too, with the DM channel itself as the conversation key.

Conversational continuity across those runs belongs to the pipeline's agent step: with session: "thread", each run's agent step continues the session keyed to that Slack thread, so the Agent remembers the conversation even though every message was its own run.

A reply posts back into the originating thread only when the Workflow's configuration declares one — an onComplete.slackReply template, rendered against the run's final scope when the run succeeds. A Workflow that declares none posts nothing, and failed or canceled runs never post. Declared replies are delivered at-least-once — a crash between posting and recording the delivery can cause a rare duplicate, which is expected behavior. Slack's own event retries are separately deduplicated by event_id.

Schedule

A cron expression fires the Workflow on a schedule, evaluated in UTC at minute precision by a five-field evaluator — minute, hour, day-of-month, month, and day-of-week, numeric only, with no month or day names. It supports *, */n steps, single values, and a-b ranges (including stepped ranges like a-b/n); when both day-of-month and day-of-week are restricted, a date matches if either one does, the standard cron convention. See Build a scheduled Workflow for worked examples.

The schedule ticker checks every 30 seconds by default and always advances the next fire time from now, never backfilling. Ticks missed while the platform is down are skipped, not queued — a control plane that's been down across several windows fires once on recovery and resumes its normal cadence, rather than bursting out every missed run at once. And a tick that finds the previous run still going is skipped for overlap without un-advancing the schedule. Both are the safer default for anything that isn't idempotent.