CastHQ specChange envelopeConditionsConfidenceReconciliation

Condition language v1

A condition is a small JSON value that says what an agent is waiting for. CastHQ evaluates it against each change envelope on the target and fires when it becomes true. No code, no regular expressions, no JSON paths: every leaf names a field from a fixed list. Schema: /v1/condition.json.

Condition := Leaf | { "all": [Condition] } | { "any": [Condition] } | { "not": Condition }

Leaf :=
  | { "change": Kind | [Kind], "semantic"?: [Semantic], "origin"?: [Origin] }
  | { "change": "schedule", "moved_by": { "gt" | "gte": Duration }, "direction"?: "later" | "earlier" | "either" }
  | { "change": "dispute", "opened"?: boolean, "loser_origin"?: [Origin] }
  | { "status": { "becomes"?: Status, "from"?: Status, "in"?: [Status] } }
  | { "availability": { "becomes"?: Availability, "from"?: Availability, "in"?: [Availability] } }
  | { "lineup": { "includes": TopicUrn } }
  | { "state":  { "becomes"?: State, "in"?: [State] } }
  | { "fact": "schedule" | "status" | "location" | "availability" | "lineup",
      "confidence": { "becomes"?: Level, "becomes_at_least"?: Level, "at_least"?: Level, "drops_below"?: Level } }
  | { "fact": "schedule" | "status" | "location" | "availability" | "lineup",
      "sources": { "at_least": N } | { "becomes_at_least": N } }
  | { "precision": { "becomes_at_least": Precision } }
  | { "listed": { "becomes": boolean } }
  | { "time": { "anchor": "start" | "end" | "deadline" | "on_sale", "offset": Duration,
                "when_imprecise"?: "wait" | "earliest" | "latest", "tz"?: IanaZone } }
  | { "time": { "at": DateTime, "tz"?: IanaZone } }
  | { "dependency": { "at_risk"?: true, "parent_state"?: { "becomes": State } } }
  | { "variant": { <axis>: Term | Term[] } }
  | { "members":  ({ "all" | "any": Level } | { "count_at_least": N, "of": Level }), "where"?: { "variant": … } }
  | { "upstream": { "all" | "any": Level } | { "count_at_least": N, "of": Level } }

Level := status "in", availability "in", lineup "includes", state "in", confidence "at_least", precision "at_least",
         sources "at_least", and "all" / "any" / "not" of those

The top-level object may carry "v": 1. Unknown keys and values are refused with the path to the mistake.

In plain words Condition
when it is released {"state": {"becomes": "released"}}
when the date is confirmed {"fact": "schedule", "confidence": {"becomes_at_least": "confirmed"}}
when the date moves by more than 3 days {"change": "schedule", "moved_by": {"gt": "P3D"}}
when it is cancelled or postponed {"any": [{"status": {"becomes": "cancelled"}}, {"status": {"becomes": "postponed"}}]}
when it is held (a rain delay, a launch hold) {"status": {"becomes": "delayed"}}
when it resumes after a hold {"status": {"from": "delayed"}}
when it sells out {"availability": {"becomes": "sold_out"}}
when my team is added to the line-up {"all": [{"change": "lineup"}, {"lineup": {"includes": "urn:casthq:topic:…"}}]}
when venue, date and line-up are all confirmed {"all": [{"fact": "location", "confidence": {"at_least": "confirmed"}}, {"fact": "schedule", "confidence": {"at_least": "confirmed"}}, {"fact": "lineup", "confidence": {"becomes_at_least": "confirmed"}}]}
1 hour before tickets go on sale {"time": {"anchor": "on_sale", "offset": "-PT1H"}}
when an event under a topic is announced {"listed": {"becomes": true}}
when a community source contradicts the producer {"change": "dispute", "opened": true, "loser_origin": ["observed"]}
when it moves later and the date is still confirmed {"all": [{"change": "schedule", "semantic": ["moved_later"]}, {"fact": "schedule", "confidence": {"at_least": "confirmed"}}]}
when three independent sources agree on the date {"fact": "schedule", "sources": {"becomes_at_least": 3}}
2 hours before start {"time": {"anchor": "start", "offset": "-PT2H"}}
not released by 1 March 2027, 09:00 Paris {"all": [{"time": {"at": "2027-03-01T09:00:00+01:00"}}, {"not": {"state": {"in": ["released"]}}}]}
when both have shipped (target events:a,b) {"members": {"all": {"state": {"in": ["released"]}}}}
the first of them to ship (mode: once) {"members": {"any": {"state": {"in": ["released"]}}}}
when everything upstream has ended (target chain:b) {"upstream": {"all": {"state": {"in": ["ended"]}}}}
1 hour before my region's maintenance ends (target variants:p) {"all": [{"time": {"anchor": "end", "offset": "-PT1H"}}, {"variant": {"region": ["euw"]}}]}
all regions back (target variants:p) {"members": {"all": {"state": {"in": ["ended"]}}}}
all EU shards back (target variants:p) {"members": {"all": {"state": {"in": ["ended"]}}, "where": {"variant": {"region": ["euw", "eune"]}}}}

Values

Limits

Limit Value
Nodes (leaves and the all / any / not around them) 16
Depth (a bare leaf is 1) 4
Values in any list (including all / any children) 8
Durations 1 minute to 400 days (an offset may also be zero)
time leaves per condition 1
members / upstream leaves per condition 1
Events in a set target (events:) 2 to 32

Evaluation

A condition is judged against one envelope at a time, and only against what the envelope carries: its kind, semantic, old / new, delta, provenance origin and the before / after snapshots stored on the change. Never against today's state, so replaying a condition over history answers "what would have fired at the time".

Initial

A subscription created when its condition is already true would otherwise wait forever ("tell me when it is cancelled", asked after the cancellation). initial decides:

Modes

Firing ids

A firing's id is deterministic: evf_<subscription>_<change>, or evf_<subscription>_initial. Reading the same range twice gives the same ids, so deduplicate on them.

Try one

POST https://api.casthq.co/v1/conditions/test replays a condition over an event's or topic's history and returns the changes it would have fired at. No sign-in needed for public events.