Implications
Implications are reactive: when one event happens, they trigger another. All implied events are written atomically with the trigger event. Order placed? Reserve inventory. Low stock? Reorder. User upgraded? Grant features.
This guide covers practical patterns. For full syntax details, see Implications reference.
Basic implication
Define in your spec:
{
"aggregate_types": {
"order": {
"events": {
"was_placed": {
"schema": {"type": "object", "properties": {"items": {"type": "array"}}},
"handler": [{"set": {"target": "", "value": "$.data"}}],
"implications": [
{
"emit": {
"aggregate_type": "notification",
"id": "admin",
"event_type": "was_queued",
"data": {"message": "New order received"}
}
}
]
}
}
}
}
}
When order.was_placed fires, j17 emits notification.admin.was_queued with the given data.
Conditional implications
Only trigger when conditions are met:
{
"was_paid": {
"schema": {"type": "object", "properties": {"amount": {"type": "number"}}},
"handler": [{"set": {"target": "", "value": "$.data"}}],
"implications": [
{
"condition": {"gte": ["$.data.amount", 100]},
"emit": {
"aggregate_type": "loyalty",
"id": "$.metadata.actor.id",
"event_type": "had_points_credited",
"data": {"points": 10}
}
}
]
}
}
Orders of $100 or more credit loyalty points. Conditions use the same predicate syntax as handlers.
Cross-aggregate implications
Implications can target different aggregate types:
{
"user": {
"events": {
"had_tier_upgraded": {
"schema": {"type": "object", "properties": {"new_tier": {"type": "string"}}},
"handler": [{"set": {"target": "tier", "value": "$.data.new_tier"}}],
"implications": [
{
"condition": {"equals": ["$.data.new_tier", "premium"]},
"emit": {
"aggregate_type": "features",
"id": "$.metadata.actor.id",
"event_type": "had_premium_enabled",
"data": {"enabled_at": "$.metadata.timestamp"}
}
}
]
}
}
}
}
User upgrade triggers a feature flag event on a different aggregate.
Dynamic target IDs
Use JSONPath to route implied events dynamically:
{
"emit": {
"aggregate_type": "user_timeline",
"id": "$.metadata.actor.id",
"event_type": "had_activity_added",
"data": {"event": "$.key", "type": "$.type"}
}
}
State-based conditions
Access the source aggregate's current state in conditions:
{
"condition": {"equals": ["$.state.notifications_enabled", true]},
"emit": {
"aggregate_type": "notification",
"id": "$.metadata.actor.id",
"event_type": "was_queued",
"data": {}
}
}
When accessing $.state.* in implications, you get the aggregate's state before any events in the current request are applied (S0). In a batch write, all implications in the batch see the same pre-batch state. The trigger event's data is available via $.data.*.
Fan-out with map
Emit one event per item in an array:
{
"map": {
"in": "$.data.items",
"as": "$item",
"emit": {
"aggregate_type": "inventory",
"id": "$item.product_id",
"event_type": "was_reserved",
"data": {
"quantity": "$item.qty",
"order_id": "$.key"
}
}
}
}
Map with condition filter
Only emit for items matching a condition:
{
"map": {
"in": "$.data.items",
"as": "$item",
"condition": {"equals": ["$item.priority", "high"]},
"emit": {
"aggregate_type": "warehouse",
"id": "$item.product_id",
"event_type": "needs_urgent_pick",
"data": {"qty": "$item.qty"}
}
}
}
Data template operators
For complex data transformations, use DSL operators in your data template.
concat - String concatenation
{
"data": {
"message": {"concat": ["Order ", "$.key", " was placed"]}
}
}
coalesce - First non-null value
{
"data": {
"name": {"coalesce": ["$.data.display_name", "$.data.name", "Unknown"]}
}
}
merge - Shallow object merge
{
"data": {"merge": [
"$.state.defaults",
{"override": "value", "timestamp": "$.metadata.timestamp"}
]}
}
Later values override earlier ones. Operators can be nested.
Scheduled implications
Delay the triggered event. See the scheduled events guide for full details.
{
"was_placed": {
"implications": [
{
"schedule": {
"delay": "24h",
"emit": {
"aggregate_type": "notification",
"id": "$.metadata.actor.id",
"event_type": "cart_abandonment_reminder",
"data": {"cart_id": "$.key"}
},
"cancel_on": [
{
"aggregate_type": "cart",
"id": "$.key",
"event_type": "was_checked_out"
}
]
}
}
]
}
}
If the cart is checked out within 24 hours, the reminder is automatically canceled.
Safety limits
Implications have built-in protection against runaway chains:
| Limit | Default | Description |
|---|---|---|
max_depth |
5 | Maximum chain depth (A implies B implies C implies D implies E) |
max_total |
100 | Maximum total implied events per trigger |
Exceeding limits returns an error; the entire transaction (trigger + implied) is rejected.
The engine detects cycles at spec validation time. A spec with A:e1 implying B:e2 implying A:e1 will be rejected.
Audit trail
All implied events include metadata for traceability:
{
"implied_by": {
"key": "order:abc123",
"event_type": "was_placed",
"depth": 1
}
}
Error handling
Implication failures don't block the original event. The parent event succeeds, implications are retried separately.
Common patterns
Inventory management
{
"aggregate_types": {
"order": {
"events": {
"was_placed": {
"handler": [{"set": {"target": "", "value": "$.data"}}],
"implications": [
{
"map": {
"in": "$.data.items",
"as": "$item",
"emit": {
"aggregate_type": "inventory",
"id": "$item.sku",
"event_type": "had_reservation_requested",
"data": {
"order_id": "$.key",
"quantity": "$item.quantity"
}
}
}
}
]
}
}
},
"inventory": {
"events": {
"had_reservation_requested": {
"handler": [{"decrement": {"target": "available", "value": "$.data.quantity"}}],
"implications": [
{
"condition": {"gte": ["$.state.available", "$.data.quantity"]},
"emit": {
"aggregate_type": "inventory",
"id": "$.key",
"event_type": "was_reserved",
"data": {"quantity": "$.data.quantity"}
}
},
{
"condition": {"lt": ["$.state.available", "$.data.quantity"]},
"emit": {
"aggregate_type": "order",
"id": "$.data.order_id",
"event_type": "had_backorder_created",
"data": {"sku": "$.key", "quantity": "$.data.quantity"}
}
}
]
}
}
}
}
}
Notifications
{
"was_posted": {
"handler": [{"set": {"target": "", "value": "$.data"}}],
"implications": [
{
"emit": {
"aggregate_type": "notification",
"id": "$.data.post_author_id",
"event_type": "was_created",
"data": {
"type": "new_comment",
"post_id": "$.data.post_id",
"commenter": "$.metadata.actor.id"
}
}
}
]
}
}
Audit logging
{
"was_created": {
"handler": [{"set": {"target": "", "value": "$.data"}}],
"implications": [
{
"emit": {
"aggregate_type": "audit",
"id": "global",
"event_type": "had_entry_added",
"data": {
"action": "create",
"target": "$.key",
"actor": "$.metadata.actor",
"timestamp": "$.metadata.timestamp"
}
}
}
]
}
}
Testing implications
Write events in staging, verify implications fire:
# Place order
curl -X POST https://myapp-staging.j17.dev/order/123/was_placed \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"items": [{"sku": "widget_1", "quantity": 2}]}}'
# Check inventory reservation fired
curl https://myapp-staging.j17.dev/inventory/widget_1 \
-H "Authorization: Bearer $API_KEY"
Compared to sagas
| Implications | Sagas | |
|---|---|---|
| Trigger | Automatic on event | Explicit (trigger event) |
| Scope | Single event chain | Multi-step workflow |
| Compensation | Manual (write compensating events) | Built-in |
| Use case | Simple reactions | Complex business processes |
Use implications for simple if-this-then-that. Use sagas for multi-step workflows with compensation.
When not to use implications
You need immediate confirmation -- Implications are atomic with the trigger, but if you need the implied event's response before returning to the client, use a saga.
Complex conditional logic -- More than 3-4 conditions? Consider a WASM handler or saga.
External calls needed -- Both Tick and WASM implications are pure data transformations with no network calls or I/O. Fetch external data before writing the trigger event.
See also
- Implications reference - Full syntax details
- Sagas guide - Complex workflows
- Scheduled events - Delayed execution