Triggers

The eight trigger types that start automation flows, from data events and webhooks to cron schedules and manual actions.

#Triggers

Every flow has exactly one trigger — the event that starts it. The trigger determines when the flow runs, what data arrives in $trigger, and whether the flow blocks the operation that caused it.

A flow only fires when it is active and has a published version. Draft flows never run from triggers — test them with a dry run instead.

#Trigger types

Type When it fires Blocking
event_action After a record is created, updated, or deleted No
event_filter Before a record write completes Yes
webhook When an external request hits the flow's public webhook URL No
schedule On a cron schedule No
manual When a user runs the flow from the dashboard or the execute API No
api_endpoint When an authenticated request calls the flow's custom endpoint No
another_flow When another flow runs a trigger_flow step targeting this flow Depends on the caller
app_event Application lifecycle events emitted by the platform: user.create, user.login, file.upload No

#Data event triggers

Data event triggers react to record changes in your collections. Both types share the same configuration:

Setting Description
events Which events to match: record.create, record.update, record.delete, or * for all
collections Which collections to watch. * matches every collection
expand Relation field names to auto-expand — the linked records are fetched and delivered in $trigger.expanded
on_failure event_filter only — what happens to the write when the filter itself fails. block (default) or allow. See failure handling

The difference is timing:

  • event_action runs after the write succeeds. It runs in the background and never delays the API response. Use it for notifications, syncing, and follow-up work.
  • event_filter runs before the write completes and blocks it. If a filter flow ends with an error — a throw_error step or a stop step with an error status — the write is rejected and the API caller receives the error. Use it for validation and screening.
Warning

Filter flows add their execution time to every matching write request. Keep them short, and move anything slow into an event_action flow.

#Rejecting a write

The rejected request answers with the error code blocked_by_automation and the message from your throw_error or stop step:

{ "error": "blocked_by_automation", "message": "Spam detected" }

The status is 403 by default. A throw_error step may name its own — statusCode: 422 for a validation-shaped rejection — and that status is used instead. The error code stays blocked_by_automation either way, so clients can match on it without tracking which status a particular flow chose.

#Filter failure handling

A filter is a gate, so by default anything that stops it from returning a clean pass rejects the write. That covers all of:

What happened Default
A throw_error step ran Write rejected
A stop step with an error status ran Write rejected
A step failed under error_handling: stop_on_first Write rejected
The flow hit its timeout Write rejected
The flow could not start (e.g. its owner was deleted) Write rejected

Set on_failure: 'allow' in the trigger config to invert this, for filters that enrich rather than enforce and shouldn't take writes down when they break:

{ "events": ["record.create"], "on_failure": "allow" }

The failure is still recorded on the run and logged; only the write is permitted.

Note

A filter cannot currently rewrite the data being saved — it approves or rejects the write as

submitted. To change stored values, use an event_action flow to update the record after the write, or reject the write and have the client resubmit.

For update and delete events the trigger payload carries change context: the record state before the change, the list of changed fields, and an isNew flag. See the trigger payload below.

#Webhook triggers

A webhook trigger gives the flow a public URL that external services can call — no EmuView authentication required, with an optional shared secret. Creating a flow with trigger_type: webhook automatically creates the endpoint and its token.

Webhooks are covered in depth in the webhooks guide.

#Schedule triggers

Schedule triggers run the flow on a cron schedule. The configuration takes a standard 5-field cron expression:

{
	"trigger_type": "schedule",
	"trigger_config": { "cron": "0 9 * * 1-5" }
}

The five fields are minute hour day-of-month month day-of-week (weekday 0 = Sunday). Supported syntax:

Syntax Example Meaning
Exact value 0 9 * * * Every day at 09:00
List 0 9,17 * * * At 09:00 and 17:00
Range 0 9 * * 1-5 Weekdays at 09:00
Step */15 * * * * Every 15 minutes

When both day-of-month and day-of-week are restricted, either can match — the standard cron OR rule.

Note

Cron expressions are evaluated in UTC. Convert your local time to UTC when writing the expression.

Each scheduled flow gets its own scheduler instance (a Cloudflare Durable Object), which executes the flow and then calculates the next run time. Runs appear in the run log like any other execution.

A Durable Object can lose its pending alarm — during a platform incident, or when its state is evicted. The instance's hourly maintenance tick re-arms the alarm for every active system schedule flow, so a lost alarm costs you at most an hour rather than stopping the flow permanently. That recovery is the tick's job, which means a stalled tick and a stalled schedule flow look the same from here: if a scheduled flow has quietly stopped firing, check System → Monitor → Health before digging into the flow itself.

#Manual triggers

Manual triggers let people run a flow on demand from the dashboard. The configuration controls where the action appears and what input it collects:

Setting Options Description
manual_type collection_action, item_action, dashboard_action Where the action button appears
collections collection names Which collections show the action (for collection and item actions)
selection none, single, multiple Whether the user selects records first — selected records arrive in $trigger.selection
input_fields field definitions A form shown before the flow runs; values arrive in $trigger.body
confirm true / false Ask for confirmation before running
confirm_message text The confirmation prompt

You can also run any active flow programmatically. The request body becomes $trigger.body:

#HTTP

POST /api/v1/automate/flows/01HXK5M8Y2W7Q4R9T3V6B1N0ZC/execute
Authorization: Bearer sk-your-api-key
Content-Type: application/json

{ "reportMonth": "2026-06" }

#SDK

import { EmuView } from '@emuview/sdk';

const sdk = new EmuView({
	url: 'https://your-api.example.com',
	auth: { apiKey: 'sk-your-api-key' }
});

// Returns immediately with a run ID — the flow executes in the background
const { data } = await sdk.automate.executeFlow('01HXK5M8Y2W7Q4R9T3V6B1N0ZC', {
	reportMonth: '2026-06'
});
console.log(data.runId, data.status); // "…", "running"

The response is 202 Accepted with a runId. Fetch the run afterwards to see step-by-step results.

#API endpoint triggers

An api_endpoint trigger turns a flow into a custom REST endpoint at /api/v1/automate/endpoint/:flowId. Unlike webhooks, callers must authenticate with EmuView credentials, and any HTTP method is accepted. The request body, headers, and method arrive in the trigger payload.

const result = await sdk.automate.callEndpoint('01HXK5M8Y2W7Q4R9T3V6B1N0ZC', {
	action: 'generate_report',
	params: { startDate: '2026-06-01' }
});
// { success: true, runId: "…", message: "API endpoint flow execution started." }

Like manual execution, the endpoint responds 202 straight away and the flow runs in the background.

#Restricting who may invoke an endpoint

By default any authenticated principal can call any active api_endpoint flow. To expose a privileged flow (for example, one that writes bans) safely, add invocation authorization to the trigger config — it is enforced on the calling identity, before the run starts (and before run_as is applied):

Field Description
required_role A role name, or an array of role names. The caller's role must be one of these. super_admin always passes.
required_permission { "resource": "system/bans", "action": "create" } — the caller must hold this RBAC grant.

When both are set, both must pass. A caller that fails receives 403 before the flow dispatches, so a privileged flow no longer needs a hand-rolled role guard as its first step.

// trigger_config on a ban-writing api_endpoint flow
{
	"method": "POST",
	"required_permission": { "resource": "system/bans", "action": "create" }
}

#Flow-to-flow triggers

A flow with trigger_type: another_flow can only be started by a trigger_flow step in a different flow. The calling step passes a payload, which arrives as $trigger.body, and chooses whether to wait for the result or fire and forget.

Sub-flows can nest up to 8 levels deep, and circular calls are detected and rejected.

#The trigger payload

Every operation in the flow can read the trigger through $trigger — in templates as {{$trigger.body.title}} or in expressions as $trigger.body.title.

Field Available for Description
event all The event name, e.g. record.create, webhook.incoming, manual
type all The trigger type
body all The main data: the record, request body, or manual input
collection data events The collection the event belongs to
previous data events The record before the change — null for creates, the full record for deletes
changed data events Names of fields whose values changed (empty for creates)
isNew data events true when the event is record.create
expanded data events Auto-expanded relation records, keyed by relation field name (when expand is configured)
method webhook, API endpoint The HTTP method of the incoming request
headers API endpoint The HTTP request headers
selection manual The records the user selected before running the flow

#Who the flow runs as

Operations respect access control, so the identity a flow runs as matters. Each flow has a Run As policy:

  • Triggering user (default) — the flow runs with the permissions of whoever caused the trigger. Webhook-triggered flows use a least-privilege synthetic webhook identity, so by default they can do very little. Scheduled flows have no triggering user, so they run as the flow owner.
  • Flow owner — the flow runs as the user who created it, regardless of who triggered it.
  • Specific role — the flow runs with a chosen role.

If the Run As identity can't be resolved (for example, the owner was deleted), the run fails rather than falling back to broader permissions.

Tip

A scheduled flow only has the permissions of its owner's role. If a scheduled run starts failing with permission errors, grant the owner's role access to the collections the flow touches, or set the flow's Run As to a role that has it.

#Gotchas

  • Editing a flow doesn't change what runs — triggers execute the published version. Publish after editing.
  • event_filter flows run sequentially and block the write; several slow filter flows on the same collection compound the delay.
  • Flows have a default timeout of 30 seconds. A scheduled flow that regularly needs longer should be split, with trigger_flow chaining the parts.