Triggers and actions
The full catalog of workflow triggers, actions, and conditions with their inputs and outputs.
Every node in a workflow graph is one of three kinds, identified by a dotted ID whose prefix names the kind:
| Kind | Prefix | Count |
|---|---|---|
| Trigger | triggers. | 62 |
| Action | actions. | 19 |
| Condition | conditions. | 3 |
A workflow needs at least one trigger. Actions and conditions are optional, but a graph with no action does nothing observable.
You pick these from the console's node palette rather than writing them, but the IDs are what a workflow's exported JSON contains — so this is the table you read when inspecting an export or a stored flow:
{
"nodes": [
{ "id": "n1", "kind": "trigger", "type": "triggers.form.submitted" },
{ "id": "n2", "kind": "condition", "type": "conditions.if_else" },
{ "id": "n3", "kind": "action", "type": "actions.send_email" }
],
"edges": [
{ "source": "n1", "target": "n2" },
{ "source": "n2", "target": "n3", "sourceHandle": "true" }
]
}sourceHandle is how a branch is chosen: condition nodes emit true and
false, and actions.wait_for_event emits event_received and timeout. An
edge with no handle is the default path.
Fetch a workflow's flow with GET /api/workflows/:id/export.
Triggers
Audience
Thirteen triggers covering membership, tags, lists, and compliance state.
| ID | Fires when |
|---|---|
triggers.audience.member_created | A new audience member is created |
triggers.audience.member_updated | Any member field changes |
triggers.audience.attribute_changed | A specific custom attribute changes |
triggers.audience.added_to_list | Added to an audience list |
triggers.audience.removed_from_list | Removed from an audience list |
triggers.audience.tag_added | A tag is applied |
triggers.audience.tag_removed | A tag is removed |
triggers.audience.unsubscribed | Opted out |
triggers.audience.resubscribed | Opted back in |
triggers.audience.blocked | Blocked |
triggers.audience.unblocked | Unblocked |
triggers.audience.email_invalidated | Address marked invalid, usually after a hard bounce |
triggers.audience.import_completed | A bulk import finished |
Email engagement
| ID | Fires when |
|---|---|
triggers.email.sent | A message is dispatched |
triggers.email.opened | The tracking pixel loads |
triggers.email.clicked | A tracked link is followed |
triggers.email.bounced | Delivery hard-fails |
triggers.email.unsubscribed | Recipient opts out from this message |
triggers.email.campaign_sent | A whole campaign finishes sending |
opened and clicked depend on tracking being enabled for the template.
Neither is reliable enough to gate access on — image blocking suppresses opens.
Subscription
| ID | Fires when |
|---|---|
triggers.subscription.created | First subscription on a workspace |
triggers.subscription.updated | Any subscription field changes |
triggers.subscription.upgraded | Moved to a higher plan |
triggers.subscription.canceled | Canceled immediately |
triggers.subscription.cancel_scheduled | Cancellation set for period end |
triggers.subscription.resumed | Reactivated after cancellation |
triggers.subscription.suspended | Suspended, typically for non-payment |
triggers.subscription.trial_started | Trial begins |
triggers.subscription.trial_will_end | Trial is approaching its end |
triggers.subscription.trial_expired | Trial ended without conversion |
cancel_scheduled and canceled are distinct. A win-back sequence should listen
for cancel_scheduled, which still has time to act.
Payment, credits, quota
| ID | Fires when |
|---|---|
triggers.payment.succeeded | Payment captured |
triggers.payment.failed | Payment declined |
triggers.payment.action_required | Payment needs 3DS or similar |
triggers.credit.purchased | Credits bought |
triggers.credit.granted | Credits issued without payment |
triggers.credit.consumed | Credits spent |
triggers.credit.expired | Credits lapsed |
triggers.credit.low_balance | Balance crossed the low threshold |
triggers.quota.limit_exceeded | A metered quota was exceeded |
User and organization
| ID | Fires when |
|---|---|
triggers.user.registered | Account created |
triggers.user.email_verified | Address confirmed |
triggers.user.logged_in | Successful sign-in |
triggers.user.password_changed | Password updated |
triggers.user.password_reset_requested | Reset requested |
triggers.user.profile_updated | Profile fields changed |
triggers.organization.member_invited | Org invite sent |
triggers.organization.member_accepted | Org invite accepted |
triggers.organization.member_removed | Org member removed |
Workspace
| ID | Fires when |
|---|---|
triggers.workspace.created | Workspace created |
triggers.workspace.updated | Workspace fields changed |
triggers.workspace.deleted | Workspace deleted |
triggers.workspace.member_added | Member joins |
triggers.workspace.member_removed | Member leaves |
triggers.workspace.member_role_changed | A member's role changes |
triggers.workspace.settings_updated | Settings changed |
triggers.workspace.features_updated | Feature flags toggled |
Forms, push, workflows
| ID | Fires when |
|---|---|
triggers.form.submitted | A form is submitted |
triggers.push.campaign_sent | A push campaign finishes sending |
triggers.workflow.published | A workflow version is published |
triggers.workflow.paused | A workflow is paused |
triggers.workflow.resumed | A workflow is resumed |
triggers.workflow.instance_completed | An instance finishes successfully |
triggers.workflow.instance_failed | An instance fails |
The triggers.workflow.* family lets one workflow react to another — useful for
alerting on repeated failures without polling.
Actions
Email and push
| ID | Notes |
|---|---|
actions.send_email | Two modes: Use Template or Custom Content. Outputs delivery status plus engagement fields (Email Opened, Link Clicked, Email Bounced, Open Count) that populate as they happen |
actions.send_push | Targets an audience member, a workspace, or a user. Outputs Delivered, Status, Skipped, Skip Reason |
Both report Skipped with a Skip Reason rather than failing when a recipient
is unsubscribed or has no push subscription. A skipped send is not an error and
does not consume a retry.
Timing
| ID | Notes |
|---|---|
actions.delay | Three modes: fixed duration (minutes, hours, days), until a time of day, or until the next weekday — including a "next business day" option |
actions.wait_for_event | Pauses the instance until a matching event arrives or a timeout fires |
wait_for_event is the branching primitive for engagement flows. It exposes two
outgoing handles, event_received and timeout, so "opened the email" and
"did not open within three days" are separate paths.
Match conditions use three operators: eq, exists, and contains. Timeout
can be a fixed duration, a time of day, or no timeout at all — an instance
waiting forever stays running and counts against re-entry rules.
Audience and CRM
| ID | Purpose |
|---|---|
actions.add_tag / actions.remove_tag | Tag management. Outputs include Was Already Tagged-style flags |
actions.audience_list_add / actions.audience_list_remove | List membership |
actions.audience_subscribe / actions.audience_unsubscribe | Opt-in state, with a reason on unsubscribe |
actions.audience_block / actions.audience_unblock | Blocking, with a reason |
actions.audience_mark_invalid | Flag an address as undeliverable |
actions.update_audience | Update name, email, language, country, timezone, currency, or custom attributes |
actions.grant_credits | Grant credits to a workspace. Outputs Amount Granted and Balance After |
Every compliance action outputs a "was already in that state" boolean, so a workflow can branch on whether it actually changed anything rather than re-notifying someone who was already unsubscribed.
Lookup and data
| ID | Purpose |
|---|---|
actions.get_audience | Look up a member by ID or email. Outputs the full profile — tags, lists, country, timezone, custom attributes — plus Found |
actions.get_workspace | Look up a workspace. Outputs member count, owner email, subscription status, plan name, billing currency |
actions.data_transform | Extract a field, parse JSON, format a date, evaluate a math expression, replace or split a string, change case, render a template, or set a static value |
Found is a boolean output, not a failure. A lookup that matches nothing
continues the flow — branch on Found with a condition rather than assuming the
instance stops.
Integrations
| ID | Purpose |
|---|---|
actions.http_webhook | Call your own API. See webhook actions |
Conditions
| ID | Behavior |
|---|---|
conditions.if_else | Branches on a field check. Handles: true, false |
conditions.filter | Continues only when the check passes, otherwise ends this path |
conditions.split_random | Percentage split, for A/B testing a sequence |
conditions.split_random is the only non-deterministic node in a workflow. The
same subject re-entering a workflow can take a different branch, so do not use
it where a stable assignment matters.
Merge tags
Any input marked allowMergeTags accepts {{path}} referring to the trigger
payload or an earlier node's outputs. The console offers only tags that are
actually reachable from that node.
Publishing validates every reference, and an invalid one is a blocking error — not a warning. Two cases are rejected:
- the tag points at a node that has been deleted
- the tag names an output the referenced node does not produce
So a broken merge tag stops the publish rather than resolving to an empty string at run time. Note this is stricter than the email template engine, where an unknown tag silently becomes an empty string.
Next Steps
- Webhook actions — receive an
actions.http_webhookcall. - Monitoring — instances, retries, and the dead-letter queue.
- Workflows overview — lifecycle and re-entry.