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
Implications have access to all standard event paths ($.key, $.id, $.type, $.data.*, $.metadata.*, @.*). Use them 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": ["@.notifications_enabled", true]},
"emit": {
"aggregate_type": "notification",
"id": "$.metadata.actor.id",
"event_type": "was_queued",
"data": {}
}
}
When accessing @.* 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.*. ($.state.* is a deprecated alias for @.*.)
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": [
"@.defaults",
{"override": "value", "timestamp": "$.metadata.timestamp"}
]}
}
Later values override earlier ones. Operators can be nested (e.g., a merge element can contain a coalesce), up to 32 levels deep.
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 |
| Template nesting | 32 | Maximum depth of nested template operators (merge/concat/coalesce) |
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
}
}
Ordering and consistency
Implied events are written within microseconds of the trigger event, but they do not guarantee they will be the next event on the target aggregate's stream. Another write to the same target aggregate could land between the trigger and the implied event. Design your implied event handlers to be additive (set fields, append to arrays) rather than dependent on being exactly the N+1th event.
The hash chain on each aggregate stream remains intact regardless of interleaving — chain hashes are computed at write time from the actual previous event, not from what the implication expected.
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": ["@.available", "$.data.quantity"]},
"emit": {
"aggregate_type": "inventory",
"id": "$.key",
"event_type": "was_reserved",
"data": {"quantity": "$.data.quantity"}
}
},
{
"condition": {"lt": ["@.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