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.
| Kind | What fires it | What lands in the event | Where the result goes | How it authenticates |
|---|---|---|---|---|
| Manual | The workflow editor's Run popover | No structured data — your message is kept as run provenance | The run's step timeline in the Runs tab | Your authenticated session in the app |
| Webhook | An HTTP POST to the ingress URL | The raw JSON body, verbatim, as data | The run's step timeline — the HTTP response is only an acknowledgement | The whk_ ingress token in the URL |
| Form | A submission of the rendered form | Each declared field, type-coerced and validated | The run's step timeline | The same per-workflow ingress token, embedded in the form's submit URL |
| Slack | An @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 one | Slack's request signature, verified within a replay window |
| Schedule | The platform's cron ticker, on tick | data.scheduledFor | The run's step timeline | N/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.