Pitfalls & Gotchas

1. Reserved payload field names

Never name a payload field type, ts, or entity_id. They are always shadowed by the event envelope. Rename to notification_type, created_at, record_id.

2. filter.where is a plain string

where: "it != x" — NOT where: { expr: "it != x" }. The { expr: ... } form is only valid under a rule’s when: clause. Using it under filter/remove/find causes ClassCastException: LinkedHashMap cannot be cast to String at compile time.

3. Column type inference

Omitting fields: on a projection infers every column as TEXT. eq: false queries break because Postgres won’t cast 'false' to boolean. Always declare fields: with explicit SQL types.

4. Aggregate queries need group_by + aggregate:

fields: - count: col is silently ignored. Use group_by: [scalar_key] + aggregate: { result: { count: col } } with limit: 1.

5. Deterministic IDs

Actions cannot call uuid() or random(). Build IDs deterministically:

value: "concat('checkin_', event.user_id, '_', event.show_id)"

If multiple intents can reach the same key, upstream callers must pre-compute the same ID to avoid PK collisions.

6. Cross-stream writes in core

core forbids direct cross-stream set/add/sub. Use op: submit in side_effects to invoke the target entity’s own action, which then does the write in its own core.

Exception: relationship_create/relationship_remove ARE allowed in core because they maintain an edge table, not stream state.

7. Input fields with reserved names

The same reserved-name rule applies to action input: fields. Naming an input type makes it unreadable from rules because event.type is the event-type string. Use notification_type, record_type, etc.

8. Stale IR

Unexpected column types or null values after deploy usually mean the projection worker loaded an older IR. Recompile and redeploy the projection worker and query service whenever fields:, derive:, or event payloads change.

9. default: [] on arrays

Omitting default: gives null, not []. contains(null, x) throws; size(null) errors in some paths. Always set default: [] for array fields and default: {} for object fields.

10. unique: true on relationships

unique: true deduplicates edges at the engine level, but the caller sees a silent no-op rather than an error. Pair it with an explicit preflight rule that emits a domain-specific rejection code so callers get meaningful feedback.


Next steps