Triggers
Triggers are LyftData’s registry of curated “run this now” actions. A Trigger is a named invocation with schema-validated parameters that dispatches a message to a target job. This gives teams a safer alternative to handcrafting low-level message traffic when they want to run smoke checks, one-off operational workflows, or self-serve actions on demand.
When to use Triggers
Use Triggers when you want:
- A discoverable list of “ready to run” actions for operators or teammates.
- Parameter validation (a schema) instead of free-form JSON.
- Policy and guardrails around who can invoke what.
- An auditable invocation record with a stable
invocation_idyou can poll.
If you want something to run on a schedule, use the normal scheduling primitives (including the DSL trigger input) instead of a manual invocation surface.
How Triggers work
At a high level:
- An admin publishes a Trigger definition (slug, schema, defaults, and target job).
- A user or admin invokes the Trigger with parameters.
- LyftData dispatches a tagged user-generated message to the target job.
- The invocation is tracked and can be polled by
invocation_iduntil it reaches a terminal state.
Trigger definitions can be published to one or more surfaces (UI and/or MCP). Invocation access is still enforced by server-side permissions and the trigger’s invocation policy.
Publish and manage Triggers (admin)
Triggers are intended to be curated. In most teams, admins publish and revise Triggers, and users/operators only invoke the approved set.
When publishing a Trigger, you typically define:
- Slug + display name: the stable identifier and user-facing name.
- Parameter schema + defaults: JSON schema for validation and optional default parameters.
- Target job + tag: which job receives the invocation message (and optionally a specific message tag). Use a tag if the target job needs stable message-trigger filtering.
- Publication surfaces: whether the Trigger should appear in the UI and/or as an MCP tool.
- Invocation policy: who can invoke it, and any per-user rate limits.
- Optional response slot + timeout: configure
response_slotwhen the caller should receive a structured result (see “Trigger result shaping” below). - Optional proxy policy: if enabled, the dispatch message carries a
trigger_proxyenvelope so sinks (for examplehttp-post) can capture and report a proxy-style response.
Triggers are revisioned. If you change a Trigger’s definition, you publish a new revision; you can also deactivate a Trigger so it no longer appears as invokable.
Publish and revise over the API
If you prefer to treat Triggers as a deployable artifact, the admin API (admin principals) exposes publish/revise/deactivate endpoints:
- Publish (creates revision 1):
POST /api/triggers/publish - Revise (creates a new revision):
POST /api/triggers/<slug>/revise - Deactivate (marks inactive):
POST /api/triggers/<slug>/deactivate
Example: publish a tenant-scoped Trigger with a response slot and proxy policy (note that proxy modes require response_slot):
{ "slug": "smoke_trigger", "tenant_id": "tenant-a", "display_name": "Smoke trigger", "description": "Run a smoke check and return a structured result.", "parameter_schema": { "type": "object", "properties": { "env": { "type": "string" }, "depth": { "type": "integer", "minimum": 1 } }, "required": ["env"] }, "default_params": { "depth": 1 }, "target_job_name": "smoke_trigger_handler", "target_message_tag": "smoke.invoke", "publication": { "mcp": true, "ui_admin": true, "ui_user": false }, "invoke_policy": { "allow_admin": true, "allow_user": true }, "response_slot": "smoke.reply", "response_timeout_seconds": 30, "proxy_policy": { "mode": "passthrough", "timeout_seconds": 30 }}To revise a Trigger, send the full draft again to the revise endpoint (the server does not patch-in-place; it creates a new revision). For platform-scoped Triggers, use tenant_id: "platform" (platform-scoped principals only).
Trigger Dispatch Payload (trigger_invoke)
Trigger invocations are delivered to the target job as a user-generated message whose payload (job_event) has a stable top-level shape:
{ "type": "trigger_invoke", "version": 1, "invocation_id": "…", "trigger": { "id": "…", "slug": "smoke_trigger", "revision": 3, "tenant_id": "tenant-a" }, "caller": { "username": "alice", "role": "admin" }, "params": { "env": "prod", "depth": 2 }, "requested_at_ns": 1739370000000000000}If the Trigger revision has proxy mode enabled, the payload also includes a trigger_proxy object (tenant/id/slot/nonce plus policy and bounds). Jobs can pass that object through to sinks; some sinks emit correlated trigger-proxy-response messages automatically.
In a message-triggered job run, this payload is available through ${msg|...} (see Variable Expansion).
Wire Up The Target Job
Most Trigger target jobs are message-triggered so they run only when invoked.
Example: a single-event job that runs on a tag and can read parameters via ${msg|job_event.params.*}.
name: smoke_trigger_handler
input: echo: trigger: message: filter-kind: user filter-type: [user-generated] filter-tag: smoke.invoke json: true event: "{}"Trigger Result Shaping (response_slot)
By default, a Trigger invocation is dispatch-only: the server records whether the invocation message was accepted for dispatch, but it does not wait for a structured result from the workflow.
If you configure a response_slot on the Trigger revision, LyftData switches to response-slot mode:
- The server keeps the invocation state in
dispatchedafter message dispatch. - The invocation reaches a terminal state only when the server observes a correlated
trigger_responsemessage for the sameinvocation_idandresponse_slot, or when the response timeout is reached. - The invocation
resultis shaped to include the structured response payload.
Emit A trigger_response From A Job
To complete an invocation in response-slot mode, emit a trigger_response payload through the message bus.
Practical rules:
- The payload is strict: it must match the v1 envelope exactly (extra top-level keys are rejected).
response_slotmust match the Trigger revision’s configured slot.emitted_at_nsmust be a number. If you build it from${...}expansions, useconvertwithconversion: json(notnum) to preserve integer precision for nanosecond timestamps.
This pattern keeps your job’s normal output separate from the Trigger response envelope by using output.message.input-field.
name: smoke_trigger_handler
input: echo: trigger: message: filter-kind: user filter-type: [user-generated] filter-tag: smoke.invoke json: true event: "{}"
actions: - add: output-fields: trigger_response: type: trigger_response version: 1 tenant_id: "${msg|job_event.trigger.tenant_id}" invocation_id: "${msg|job_event.invocation_id}" response_slot: "smoke.reply" nonce: "${msg|job_event.invocation_id}" outcome: succeeded result: ok: true message: "Smoke check passed" emitted_at_ns: "${msg|job_event.requested_at_ns}"
- convert: conversions: - field: trigger_response.emitted_at_ns conversion: json
output: message: input-field: trigger_responseIf you need to report failure, set outcome: failed and include error_code and/or error_message (you can still include a result object for structured details).
What Callers Receive
The invoke call (UI/MCP/API) returns an invocation record. When a response is observed, the server stores it under invocation.result.trigger_response, and the invocation transitions to succeeded or failed.
If no response is observed before the configured response deadline, the invocation transitions to timed_out and the result is shaped with the deadline.
Proxy Capture (trigger_proxy_response)
If the Trigger revision has proxy mode enabled, the dispatch payload includes a trigger_proxy envelope. When that envelope is present:
- Pass the envelope through as part of the event you send to the sink (outputs read event fields, not
${msg|...}bindings). - The
http-postoutput captures allowlisted response headers and a bounded body and emits atrigger_proxy_responsemessage automatically. - The server records it under
invocation.result.trigger_proxy_response(plus atrigger_proxy_recorded_attimestamp) when the correlation matches.
Recording a proxy response does not complete the invocation by itself. If you want the invocation to reach a terminal state quickly, emit a trigger_response as well.
Example: copy the trigger_proxy object from the Trigger dispatch payload into the current event (and parse it back into JSON) so http-post can observe it:
- add: output-fields: trigger_proxy: "${msg|job_event.trigger_proxy}"
- convert: conversions: - field: trigger_proxy conversion: jsonInvoke Triggers in the UI
The Triggers UI is the “run now” surface:
- Pick a Trigger from the list.
- Fill in the schema-defined fields (defaults can be prefilled).
- Run it and follow status by
invocation_iduntil it completes.
Invoke Triggers over the API (automation)
For automation, the API exposes a list/invoke/status flow:
- List invokable Triggers for a surface (for example UI):
GET /api/triggers?surface=ui&invokable_only=true - Invoke a Trigger:
POST /api/triggers/<slug>/invoke - Fetch status/result:
GET /api/trigger-invocations/<invocation_id>
Invocations are asynchronous. If you need a definitive completion record, always poll by invocation_id, even if the initial invoke call returns quickly.
Invoke Request Body
POST /api/triggers/<slug>/invoke accepts a JSON body with optional control knobs:
{ "params": { "env": "prod", "depth": 2 }, "idempotency_key": "smoke-prod-2026-03-13T10:15:00Z", "wait_timeout_seconds": 15, "tenant_id": "tenant-a", "callback": { "url": "https://automation.example/hooks/lyftdata-trigger", "headers": { "authorization": "Bearer …" } }}Practical behavior:
paramsmust be a JSON object.nullis treated as{}.tenant_idis required when the caller has multiple tenant memberships. Platform-scoped callers use an empty tenant scope.idempotency_keydeduplicates by(tenant scope, trigger slug, caller username, idempotency_key). A retry with the same key returns the originalinvocation_idinstead of dispatching a second message.wait_timeout_secondscontrols how long the invoke endpoint waits before returning the current invocation record. Default: 15 seconds. Max: 300. It does not change how long the workflow has to produce a response.
Timeouts: Wait vs Response vs Proxy
These timeouts control different parts of the system:
wait_timeout_seconds(invoke request) bounds the API call’s “wait for a more up-to-date invocation record” behavior.response_timeout_seconds(Trigger definition, only whenresponse_slotis set) is the deadline for observing a correlatedtrigger_responsebefore the invocation becomestimed_out(default: 15 seconds).proxy_policy.timeout_seconds(Trigger definition, proxy mode) bounds the proxied sink attempt (for example, thehttp-postrequest timeout for capturing atrigger_proxy_response). Max: 300 seconds.
Callback Delivery (automation)
If you provide callback, the server posts invocation state updates to your URL as best-effort automation glue.
Callback request rules:
callback.headersmust be a JSON object of string values (max 32 headers). Invalid header names/values are rejected.- Delivery is retried up to 5 times with backoff on transport errors and retryable status codes (
408,429,5xx). - Callback delivery does not block invocation dispatch or completion.
Callback payload shape:
{ "version": 1, "event_id": "…", "event_type": "trigger_invocation_state", "emitted_at": 1739370000000000000, "invocation": { "invocation_id": "…", "trigger_slug": "smoke_trigger", "trigger_revision": 3, "tenant_id": "tenant-a", "state": "dispatched", "result": { "status": "accepted_for_dispatch", "mode": "response_slot", "state": "dispatched" } }}Invoke Triggers from MCP (dynamic tools)
When a Trigger is published for MCP, it appears as a tool in your MCP client. These tools are generated dynamically from the registry at tool-list time, and tool names are derived from the Trigger slug, for example trigger_invoke__smoke_trigger.
Operationally:
- Trigger tools are write-gated and require starting the MCP server with
--allow-write. - Trigger tools show up only if the Trigger is published for MCP and your identity is allowed to invoke it.
- The tool returns an
invocation_id; usetrigger_invocation_getto fetch status/result. - Trigger tools accept the Trigger’s schema fields plus reserved control keys. Reserved keys:
_idempotency_key(dedupe retries) and_wait_timeout_seconds(default 15, max 300).
Where to go next
- Workflows for message edges and control-plane composition.
- Message Bus for how the aggregator message model maps into job DSL (
internal-messagesandoutput.message). - Messages for the live UI stream (channels, filters, export).
- MCP Overview for setup, auth, and why trigger tools are treated as writes.