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.
| Mechanic | Fed by |
|---|---|
| Streak | A rule with streak.contribute |
| Badge | A rule with badge.contribute or badge.grant |
| Quest | A 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 |
| Leaderboard | A rule with leaderboard.contribute, or POST /v1/leaderboards/{id}/contributions |
| Referral | Its 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_definitionslists it. A browser may send it only once it isclient_allowed, which the change set below sets. - The credential that proposes needs
config:write(POST /v1/change_sets); the rule needsrules:writeto create and the publish scope (rules:publish_sandboxin a test environment,rules:publishin 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.updatechanges fields of an existing event definition: here it lets browsers sendlesson.completedfrom a signed-in session (customer_claim). Leave it out for a server-fed streak.program.createnames 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_definitionpublishes version 1 and makes the programactive, so noprogram.activateis needed (program.activatere-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:
{
"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:
definitionis the streak itself: a period counts when its contributions reachqualification.minimum(aggregationcountorsum);freezescaps how many missed periods a customer can have covered (auto_usespends one automatically);milestonesare reached at athresholdof consecutive periods, once perlifetimeor perepisode.qualification.sourceslists the events that may count, each optionally withconditions(occurrenceislifetimeby default).calendar: a streak usesperioddayorweekandrun_per_period: false. The day followstimezone_source:environment(the environment's timezone,GET /v1/environments/{id}withenvironments:read),customer(the customer's) orfixed(addtimezone).day_start_offset_minutes(default 0) moves the start of the day;grace_secondsandmax_lateness_seconds(default 0) accept late activity.event_timesays which time places an event in a period:occurred_atfor server events, the receive time for browser events.trust: browser events in a signed-in session arepublishable_eventatcustomer_claim; a secret key's aresecret_key_eventatmerchant_asserted. See Program trust policy, includingrecognition_capsto bound how much browser activity counts.reward.onmaps slots to actions; a milestone's slot ismilestone:<key>. This example pays nothing; thedaily_habitplaybook (GET /v1/playbooks/{id}with iddaily_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:
{
"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.