Orishare
Concepts

Author a mechanic through a change set

A complete, working daily streak: the change set that creates and publishes the program, the rule that feeds it, who confirms what, and when a customer first sees it.

This page builds one working mechanic end to end: a daily streak fed by a lesson.completed event. The same change set authors a badge, quest, level (tier), leaderboard or referral program, with its own kind and definition; how each kind is then fed differs (table below). Every request body on this page is checked against the API by the Orishare test apps/api/test/r11-docs-examples.test.ts, so it is exactly what the API accepts today. Paths are written with /v1 on the API origin https://api.orishare.com.

How a streak is fed

A streak advances only when a published rule runs a streak.contribute action for it (or an operator sends a direct contribution command). The program's qualification.sources is a filter, not a feed: it lists the events that may contribute, and a streak.contribute caused by any other event is refused (not_eligible). A program with qualification.sources and no rule never advances. So a working streak is two things: the program, and a rule.

MechanicFed by
StreakA rule with streak.contribute
BadgeA rule with badge.contribute or badge.grant
QuestA rule with quest.contribute naming the step (and quest.enroll), one rule per step
Level (tier)A rule with points.award into its XP currency
LeaderboardA rule with leaderboard.contribute, or POST /v1/leaderboards/{id}/contributions
ReferralIts own qualifying_event, sent with a secret key after a claim; no rule

Before you start

  • The event definition exists. It is created the first time any event of that name is sent with a secret key; GET /v1/event_definitions lists it. A browser may send it only once it is client_allowed, which the change set below sets.
  • The credential that proposes needs config:write (POST /v1/change_sets); the rule needs rules:write to create and the publish scope (rules:publish_sandbox in a test environment, rules:publish in a live environment) to publish. Proposing writes nothing live; a signed-in owner or admin confirms in the dashboard.

1. Propose the program

One change set creates the program, publishes its first version and allows the browser event. The commands run in order:

  • event_definition.update changes fields of an existing event definition: here it lets browsers send lesson.completed from a signed-in session (customer_claim). Leave it out for a server-fed streak.
  • program.create names the program (kind, key, name) and takes no definition. The key is permanent: rules and customer reads refer to it (program_key). A key is unique in the environment across kinds: a quest and a badge cannot share one.
  • program.publish_definition publishes version 1 and makes the program active, so no program.activate is needed (program.activate re-activates a paused program; what a pause stops for each mechanic is listed under Pausing a program).

POST /v1/change_sets with an Idempotency-Key:

change-set.json
{
  "commands": [
    {
      "type": "event_definition.update",
      "key": "lesson.completed",
      "definition": { "client_allowed": true, "minimum_trust": "customer_claim" }
    },
    {
      "type": "program.create",
      "kind": "streak",
      "key": "daily_lessons",
      "name": "Daily lessons"
    },
    {
      "type": "program.publish_definition",
      "key": "daily_lessons",
      "definition": {
        "schema": "orishare.program/v2",
        "kind": "streak",
        "semantics": "native_v2",
        "definition": {
          "kind": "streak",
          "qualification": { "aggregation": "count", "minimum": "1" },
          "freezes": { "ceiling": 1, "auto_use": true },
          "milestones": [{ "key": "first_week", "threshold": 7, "occurrence": "lifetime" }]
        },
        "qualification": {
          "sources": [{ "event": "lesson.completed", "occurrence": "lifetime" }]
        },
        "calendar": {
          "schema": "orishare.calendar/v1",
          "period": "day",
          "run_per_period": false,
          "timezone_source": "environment",
          "ambiguous_local_time": "earlier",
          "nonexistent_local_time": "next_valid",
          "event_time": { "merchant": "occurred_at", "client": "received_at" },
          "after_finalization": "correction_only"
        },
        "trust": {
          "minimum_trust": "customer_claim",
          "allowed_sources": ["publishable_event", "secret_key_event"],
          "on_insufficient": "ignore",
          "liability_caps": [],
          "recognition_caps": [],
          "allow_legacy_unknown": false
        },
        "correction": { "spendable": "reverse" },
        "reward": { "on": {} },
        "display": { "title": "Daily lessons" },
        "enrollment": { "mode": "automatic" }
      }
    }
  ]
}

The answer is 201 with the stored change set: status: "pending", plan.diff (here a create entry ["streak",null,"daily_lessons",null] under programs), plan.flags, plan.irreversibility (a published version stays in history), plan_digest and expires_at. Read it again any time with GET /v1/change_sets/{id}.

The definition, field by field:

  • definition is the streak itself: a period counts when its contributions reach qualification.minimum (aggregation count or sum); freezes caps how many missed periods a customer can have covered (auto_use spends one automatically); milestones are reached at a threshold of consecutive periods, once per lifetime or per episode.
  • qualification.sources lists the events that may count, each optionally with conditions (occurrence is lifetime by default).
  • calendar: a streak uses period day or week and run_per_period: false. The day follows timezone_source: environment (the environment's timezone, GET /v1/environments/{id} with environments:read), customer (the customer's) or fixed (add timezone). day_start_offset_minutes (default 0) moves the start of the day; grace_seconds and max_lateness_seconds (default 0) accept late activity. event_time says which time places an event in a period: occurred_at for server events, the receive time for browser events.
  • trust: browser events in a signed-in session are publishable_event at customer_claim; a secret key's are secret_key_event at merchant_asserted. See Program trust policy, including recognition_caps to bound how much browser activity counts.
  • reward.on maps slots to actions; a milestone's slot is milestone:<key>. This example pays nothing; the daily_habit playbook (GET /v1/playbooks/{id} with id daily_habit) pays coins at its milestones and is a working example of the slots.
  • enrollment.mode: "automatic" (the default) enrolls a customer at their first contribution.

2. Confirm, in the dashboard

When the environment's autonomy policy allows it (a test environment, auto, or a live environment on limits with the plan inside the limits), the proposer applies the change set itself with POST /v1/change_set_applications and no person is needed; under limits this change set's flag is not a refusal, only an advisory. Otherwise a person confirms it.

The proposing credential cannot confirm: POST /v1/change_set_commits with a PAT or key is 403 confirmation_required. Hand the person the change set id (and the dashboard_url when a proposing tool returned one, <dashboard>/change-sets/{id}). They open it under Operations, Changes (/changes/{id}), review the diff, acknowledge each flag and confirm.

This change set has one flag: making lesson.completed client-allowed is trust_weakening. Confirming without acknowledging it is 409 acknowledgement_required.

A proposal is valid for 24 hours (expires_at; later, 409 change_set_expired) and only while the configuration it was planned against is unchanged. Any configuration change in between, even a new rule draft, makes confirming 409 preview_stale. In both cases propose the same body again (a new Idempotency-Key) and hand over the new id; the dashboard's "Propose again" does the same.

3. Add the rule

The rule names the program by its key. Its draft can be saved at any time, but it publishes only once the program exists: before the commit, publishing is 422 rule_invalid (unknown reference daily_lessons). Create it after the commit, so the draft does not make the pending proposal stale. Body for POST /v1/rules:

rule.json
{
  "name": "Count a lesson toward the daily streak",
  "definition": {
    "schema": "orishare.rule/v2",
    "trigger": { "kind": "customer_event", "event": "lesson.completed" },
    "actions": [{ "type": "streak.contribute", "streak": "daily_lessons", "value": 1 }]
  }
}

Then publish it with POST /v1/rules/{id}/publish and If-Match: "<version>" (the version the create returned). A credential that should not publish directly proposes the same body as a change set command instead, { "commands": [{ "type": "rule.create", "name": ..., "definition": ... }] }, and after that is confirmed, { "type": "rule.publish_draft", "key": "<rule id>", "expected_version": <version> }, where the rule id is outcome.result.id and the version outcome.result.version of the first commit (What the commit returns); each is its own change set, confirmed like the first.

4. What customers see

Nothing changes for a customer until their first lesson.completed after the rule is published. That event contributes, which creates the customer's participation: from then on the streak is in GET /v1/customer/streaks (with current and best as decimal strings, freezes as a number, and the current period) and in the operator's GET /v1/streaks/{id}/participations. Before that the customer's list is empty, also while availability is available: streaks have no "available to start" row. Show a not-yet-started streak from your own copy, keyed by program_key.

Other mechanics

The same three steps author the other kinds; only kind and the definition differ, and Mechanics says what each one does. The exact grammar of every kind is the program.publish_definition command in the POST /change_sets request schema of /openapi.json, and GET /v1/playbooks/{id} returns working examples of each kind inside playbook manifests.

On this page