Orishare
Concepts

Rules

The orishare.rule/v1 document: triggers, conditions, limits, actions, versions.

A rule is a JSON document validated on create and again on publish. Publishing writes an immutable version; pausing, resuming and archiving change status without touching versions; rollback publishes an older definition as a new version. The JSON Schema is available from the rule-schema package and every validation error carries a rule_invalid.* code and a path.

{
  "schema": "orishare.rule/v1",
  "trigger": { "event": "order.paid" },
  "conditions": {
    "all": [
      { "path": "event.properties.total_cents", "op": "gte", "value": 5000 },
      { "path": "customer.tier", "op": "in", "value": ["silver", "gold"] }
    ]
  },
  "limits": [{ "id": "daily", "scope": ["customer"], "window": "day", "max": 3 }],
  "actions": [
    {
      "type": "points.award",
      "currency": "points",
      "amount": "floor(event.properties.total_cents / 100)"
    },
    { "type": "tier.evaluate", "program": "tp_…" }
  ]
}

Paths

event.id | name | occurred_at | customer_id | properties.<key>, customer.id, customer.attributes.<key>, customer.balances.<currency>, customer.tier, customer.segments, customer.counters.<name>, now.

Operators

eq neq gt gte lt lte in not_in contains starts_with exists between within_last. Conditions nest with all, any, not. A missing or mistyped value makes the leaf false and the trace records the operand and its issue; unknown never matches, so "no email on file" is { "path": "customer.attributes.email", "op": "exists", "value": false }.

Limits

{ id, scope[], window?, max }: scope is any list of customer, event.* or customer.* paths; window is none | day | week | month | year in the environment's timezone. Limits are reserved before effects, all-or-nothing per rule, and survive rule edits that keep the same id.

Actions

points.award / points.deduct (currency, amount as a number or a decimal expression, optional confirm_after, expires_after, reason), reward.issue (reward), tier.evaluate (program), attribute.set (attribute, value), webhook.emit (event, optional payload), counter.release (rule, limit). Amount expressions support + - * /, floor, ceil, round, min, max over numeric paths. Amounts are whole minor units of the currency: a fractional result is rounded half to even to an integer, and a currency's precision only says how a stored integer is displayed (precision 2 shows 125 as 1.25), it never scales an amount.

points.reverse takes back what an earlier event awarded, for refunds and cancellations:

{
  "type": "points.reverse",
  "currency": "points",
  "match": { "event": "order.paid", "on": "event.properties.order_id" },
  "fraction": "event.properties.refund_cents / event.properties.total_cents",
  "reason": "Refund"
}

match.event is the event whose awards are reversed and match.on an event.properties path both events carry with the same value. Without fraction (0 to 1, of each award) or amount (whole minor units, oldest award first) the whole award is reversed; give one or neither, not both. An award that is still pending is voided (a partial refund keeps the rest pending with the same confirmation date). A confirmed award gets a negative reverse ledger entry with reversal_of; points already spent are reported as unrecoverable, or drive the balance negative when the currency has negative_balance: allow. An award is never reversed for more than its amount, and each refund event touches an award once. Every award touched sends a points.reversed webhook. points.deduct is a plain debit and is not a refund.

webhook.emit sends a custom webhook. payload maps names to values, at most 32 of them:

{
  "type": "webhook.emit",
  "event": "order.rewarded",
  "payload": {
    "order_id": "event.properties.order_id",
    "tier": "customer.tier",
    "source": "loyalty",
    "items": 3,
    "tags": ["a", "b"]
  }
}

A string that starts with event. or customer., or is exactly now, is a path, resolved when the rule runs with the resolver conditions use; every other string, number, boolean, null and list of scalars is sent as written. Nested objects are refused, and so is a string that reads as a path but is not one (customer.nickname): both fail validation with rule_invalid.webhook_payload. A path whose value is missing, null or not a scalar is sent as null and named in the trace. The resolved payload may not exceed 4096 bytes; beyond that the rule is skipped with invalid_operand (webhook_payload_too_large). The delivery's data carries the result as payload. Without payload the delivery carries the triggering event's properties instead.

counter.release gives one reservation of a limit back, for refunds of once-only awards:

{ "type": "counter.release", "rule": "rule_01...", "limit": "per_content" }

rule and limit name the limit by rule id and limit id (l0, l1, ... when the limit has no id); an unknown rule or limit fails validation with rule_invalid.action_reference. The limit's own scope paths are resolved from the releasing event, so a content.refunded event carrying the same content_id as the purchase releases exactly that customer's and content's reservation, and a windowed limit releases the window the releasing event falls in. One is given back per event and limit key, never below zero; a release of nothing is reported as released: 0. If the only remaining count for those scope values was reserved before the Phase 0 upgrade (it sits in a legacy lim| row, which is read as already consumed and never lowered), the release applies nothing and reports limit.release_unresolved: such a reservation cannot be given back and stays consumed until its window ends; the rule's other actions still run. If the event lacks a scope value, or the named rule is not published, the rule is skipped with invalid_operand.

A rule applies all of its actions or none

A rule is one unit. When any of its actions is refused for this customer and event (for example a program contribution for a customer not enrolled in an explicit-enrollment quest, a limit already used, or an unresolvable operand), the whole rule is skipped: none of its actions apply, and the event's trace names the rule and the reason (not_enrolled, limit_exhausted, invalid_operand, ...). Put actions that must apply independently in separate rules, for example points for every customer in one rule and quest progress in another.

Campaigns

A rule may belong to a campaign, which adds a schedule, a priority, an optional exclusive stacking_group (highest priority wins), a budget shared across customers and an audience of segments. Campaign checks happen before the rule's own conditions.

Rule language v2

orishare.rule/v2 adds explicit trigger kinds, source provenance, program projections and scheduled evaluations. Existing v1 documents retain their stored definition and behavior. A v2 customer event trigger is { "kind": "customer_event", "event": "order.paid" }. A transition trigger names an immutable program key and a registered transition type:

{
  "schema": "orishare.rule/v2",
  "trigger": {
    "kind": "program_transition",
    "program": "reading_goal",
    "type": "quest.completed"
  },
  "trust": { "minimum": "merchant_asserted" },
  "actions": [{ "type": "webhook.emit", "event": "reading_goal.completed" }]
}

The named program must exist in the environment and support that transition. Publication, resume and rollback validate references, installed execution capabilities and the combined program/rule transition graph. Cycles and paths longer than four steps are rejected. A stored version keeps its program-kind bindings; rollback cannot reinterpret an old key as a different kind. A program's own reward policy runs with its transition. Rules reacting to that transition run as separate durable follow-ups, so their failure cannot undo the committed source result.

GET /v1/capabilities requires rules:read. Its schema_supported action names describe recognized syntax; execution_supported describes the actions this environment executes, which in an environment with the mechanics installed includes the streak, badge, quest, leaderboard and program actions (streak.contribute, streak.freeze_grant, badge.contribute, badge.grant, quest.enroll, quest.contribute, leaderboard.contribute, program.reverse_contribution). Publishing a rule with an action missing from execution_supported returns rule_invalid with a rule_invalid.capability_unavailable detail. Publication also checks the program the action names: a rule may contribute to a streak, a badge, a tier, a leaderboard or a step of a quest (Mechanics). A rule that names a program by key is published only once that program exists (rule_invalid.action_reference otherwise); its draft can be saved earlier. Legacy projections do not provide a second native effect writer.

item.grant (v2 only) gives the customer inventory items. item names an active item definition and quantity is 1 to 1000 (default 1):

{ "type": "item.grant", "item": "xp_boost_15m", "quantity": 1 }

It is executable in every environment, with or without mechanics, and a program reward slot (quest completion, streak milestone, badge rank, level) accepts it too. An unknown or archived item fails publication with rule_invalid.action_reference. Items are not monetary, so the action needs no liability valuation; a client event grants items only when the rule's or the program's trust floor admits it. A grant is made once per rule execution or reward slot, and the event's effects report it as item.granted (item, quantity, entry_id). When the item's max_quantity leaves less room than asked, what fits is granted and the rest is reported as not_granted; when nothing fits, or the item was archived after publication, the effect is item.not_granted with a reason and the rule's other actions still apply. Reversing an award (points.reverse, a refund) does not take back items granted with it.

V2 reads typed paths such as source.kind, source.trust, source.effective_at, source.accepted_at, transition.type, transition.before.*, transition.after.*, schedule.key, schedule.due_at and customer.quests.<key>.steps.<step>.progress. Unavailable projection coverage is different from an absent participation. A condition must not interpret unavailable coverage as zero progress. In v2, an exists condition omits value; v1 retains its existing operand syntax.

Source trust and liability

The server derives provenance from the authenticated admission path. Requests cannot set their own source or trust. Customer authentication establishes identity; it does not establish that a purchase happened. A rule may require trust: { "minimum": "merchant_asserted" }; the event definition's minimum_trust separately controls admission. Transitions and schedules inherit trust from their supporting evidence and never turn client activity into merchant proof. Historical legacy_unknown evidence satisfies no trust floor.

Client activity can grant spendable value only under an explicit liability cap with a valuation; the default allowance is zero. Caps and rule limits are enforced atomically with effects. Recognition has separate abuse limits. A refund does not replenish ordinary client allowance; source-linked release requires an explicit verified-void policy.

Scheduled rules

An inactivity trigger runs when an admitted activity becomes old enough. It does not wait for a return event. This example awards a winback allowance after 30 days without another order:

{
  "schema": "orishare.rule/v2",
  "trigger": {
    "kind": "schedule",
    "schedule": {
      "key": "lapsed_order",
      "kind": "inactivity",
      "event": "order.paid",
      "offset": "30d"
    }
  },
  "trust": { "minimum": "merchant_asserted" },
  "limits": [{ "id": "yearly", "scope": ["customer"], "window": "year", "max": 1 }],
  "actions": [{ "type": "points.award", "currency": "points", "amount": 300 }]
}

Configure order.paid admission to require merchant evidence. The last admitted order supplies the inactivity basis and its trust; the timer proves no new purchase. A newer admitted order invalidates the older timer, including while its evaluation is pending. Initial seeding uses retained events, so publishing an inactivity rule cannot infer activity older than retention. Changing the event name requires evidence for the new event name.

every schedules accept a day, week or month cadence and a local at time. Materialization covers the current period; it does not replay missed historical periods after downtime. Each period freezes its customer membership once, then processes those customers independently. Republishing a rule does not create a second allowance for that period. A period_deadline schedule names a program and waits for its persisted period to finish finalization before reading that period's projection. entitlement_expiry is recognized syntax but is unavailable for publication until its execution capability is installed.

All schedule and transition work uses the durable program queue. Execution rechecks current rule status, customer eligibility, source trust, coverage, availability, budgets and caps. A deleted customer suppresses downstream work. Schedules, follow-ups and job ticks do not add accepted-event or active-customer usage. A synchronous event response can include queued_work and pending_followups; replay preserves the original committed result.

Yearly dates

A date_attribute schedule fires once per customer and calendar year on a customer date: a collected birthday or anniversary, a key you define, or signup (the day the customer was created). attribute names the date key, at is the local time (default 09:00), offset fires that long before the day, and min_notice (default 30d) skips the year when the date was set or changed with less notice. The time is local to the customer's time zone, else the environment's, and 29 February fires on 28 February outside leap years. See Birthdays and anniversaries.

{
  "kind": "schedule",
  "schedule": {
    "key": "birthday",
    "kind": "date_attribute",
    "attribute": "birthday",
    "at": "09:00"
  }
}

Purchase and refund examples

Example 04 requires a merchant server to look up purchase_id, verify that its owner is the customer and that it grants the named content_id, then emit content.completed with purchase_verified: true. Set that event definition's minimum_trust to merchant_asserted and client_allowed to false, and enforce a schema requiring nonempty purchase and content identifiers. The v2 rule also requires merchant trust. A raw client boolean or a customer's unkeyed count of previous purchases is not proof of ownership of this content.

Example 05 reverses by event.properties.purchase_id, not content_id: two purchases of the same content have separate identities. Both content.purchased and content.refunded must carry that same purchase identifier, use merchant-only admission, and be emitted only after the server verifies customer ownership and the purchase or refund. A refund event needs its own stable event id for retries. Both examples are v2 rules with trust: { minimum: "merchant_asserted" }; Rule examples 04 and 05 shows them in full with their reversal semantics. points.reverse finds the award through retained events; native program corrections instead reference permanent contribution identity and survive raw-event retention.

Simulation

POST /rules/{id}/simulate accepts either its existing event input or a fact with kind program_transition or schedule, never both. A simulated fact includes a source projection and the corresponding transition or schedule fields. Simulation uses the same pure evaluator and dependency snapshot without committing effects. Supplying a trust level for simulation does not establish that trust for a live event or command.

On this page