Triggers
Status: [Designed] -- Area 2 of the Grove language. Syntax and semantics are defined; implementation is in progress.
Triggers connect Area 1 (aggregates) to Area 2 (workflows). When a domain event is emitted by a module, a trigger can automatically start a workflow, mapping fields from the event into the workflow's input. This is the primary mechanism for event-driven workflow orchestration in Grove.
Prerequisites: Concepts Overview, Workflows Overview. What you'll learn: How to declare triggers, map event fields to workflow inputs, and understand trigger execution semantics.
Declaring a Trigger
A trigger watches for a specific event from a module and starts a workflow when that event occurs:
trigger on_order_placed from orders.order_placed {
start fulfill_order v1 {
order_id: event.id
customer_id: event.customer_id
items: event.items
total: event.total
}
}
The declaration has three parts:
- Name --
on_order_placed. A unique name within the module for this trigger. - Source event --
from orders.order_placed. The qualified module and event name to watch. - Start block --
start fulfill_order v1 { ... }. The workflow to start and the field mappings.
Source Event
The from clause references a domain event using dot notation: module.event_name.
trigger on_payment_received from payments.payment_received {
start reconcile_payment v1 {
payment_id: event.payment_id
amount: event.amount
}
}
The trigger activates every time the specified event is emitted, regardless of which action caused it. If multiple actions can emit the same event, the trigger fires for all of them.
Cross-Module Triggers
Triggers can watch events from any module in the project, not just the module where the trigger is declared:
// In the fulfillment module, watching an event from the orders module
trigger on_order_placed from orders.order_placed {
start fulfill_order v1 {
order_id: event.id
items: event.items
}
}
This is the standard pattern for decoupling modules: the orders module emits events without knowing who consumes them, and the fulfillment module reacts to those events without knowing how they were produced.
Versioned Sources
Events evolve. When an event grows new fields or changes shape, the old form is versioned and a new version is introduced alongside it. Sources and triggers can both reference a specific event version using a .vN suffix:
// Source declared with an explicit version.
source deploy_started v1 { ... }
source deploy_started v2 { ... }
// Trigger subscribing to exactly v1.
trigger on_deploy_v1 from StudioEdge.deploy_started.v1 {
start handle_v1_deploy v1 { ... }
}
// Trigger subscribing to exactly v2.
trigger on_deploy_v2 from StudioEdge.deploy_started.v2 {
start handle_v2_deploy v1 { ... }
}
Unversioned references (from orders.order_placed without a .vN suffix) remain supported for modules that have not yet forked their event schema — the resolver treats an unversioned reference as "the single available version." When multiple versions exist for an event and a trigger references it without a version suffix, the resolver raises an ambiguity error at compile time. Add an explicit .vN to disambiguate.
Use this mechanism to:
- Keep old triggers running against the old event version while new triggers handle the new shape.
- Migrate consumers incrementally without a flag day.
- Decommission old versions once their last consumer is retired.
See Versioning and Migration for the full versioning model across records, events, and actions.
Field Mappings
The field mappings inside the start block connect event fields to workflow input fields:
start process_refund v1 {
order_id: event.order_id
refund_amount: event.amount
reason: event.reason
requested_by: meta.by
}
Available Root Objects
| Root | Description |
|---|---|
event | The emitted event's fields |
meta | Execution metadata: meta.id, meta.by, meta.timestamp, meta.action_id |
Field mappings are not general-purpose apply blocks. They are direct assignments -- you cannot use let bindings, if/else, for loops, or complex expressions. Each mapping is workflow_field: source_expression.
Execution Semantics
- Asynchronous -- Triggers do not block the action that emitted the event. The workflow is started asynchronously after the event is persisted.
- At-least-once -- The runtime guarantees the trigger fires at least once per event. In rare failure scenarios, the workflow may be started more than once. Design workflows to be idempotent.
- Per-event -- Each event emission fires the trigger independently. If an action emits three events and a trigger watches one of them, the trigger fires once. If it watches all three, it fires three times.
Multiple Triggers
A module can have multiple triggers watching different events:
trigger on_order_placed from orders.order_placed {
start fulfill_order v1 {
order_id: event.id
items: event.items
}
}
trigger on_order_cancelled from orders.order_cancelled {
start cancel_fulfillment v1 {
order_id: event.id
reason: event.reason
}
}
trigger on_payment_failed from payments.payment_failed {
start handle_payment_failure v1 {
order_id: event.order_id
error_code: event.error_code
}
}
Multiple triggers can also watch the same event, starting different workflows:
trigger on_signup_fulfillment from accounts.account_created {
start onboarding_workflow v1 {
account_id: event.id
}
}
trigger on_signup_notification from accounts.account_created {
start send_welcome_email v1 {
email: event.email
name: event.name
}
}
See Also
- Workflows Overview -- The workflows that triggers start
- Schedules -- Time-based workflow starts (the complement to event-based triggers)
- Webhooks -- External events that can also start workflows
- Concepts Overview -- Events and the action-event-record pipeline
- Full Language Reference -- Trigger grammar (section 6.14)