Policy reference
The rule schema, the condition grammar, every policy action, and the attention formula, bands and defaults exactly as configured in code.
A rule is a policy row. This page is the schema of that row, the grammar of its condition, the behaviour of each action, and the attention arithmetic those actions feed. Numbers here are the defaults in code; a program can override the attention ones in its spec.attention_config.
Routes: GET /api/v1/policies (filters project_id, episode_profile_id, status, trigger_type), POST /api/v1/policies, GET, PUT and DELETE /api/v1/policies/{policy_id}, POST /api/v1/policies/{policy_id}/test, GET /api/v1/policies/{policy_id}/executions. Reads need the Read scope; writes need Write.
The rule schema
| Field | Type | Meaning |
|---|---|---|
slug, name, description, version | string | Identity. slug is unique within its scope. |
scope | global, project, episode_profile | Which subjects the rule sees. Inferred from episode_profile_id or project_id when omitted. A global rule sees every project in its environment. |
project_id, episode_profile_id | int, uuid | The scope anchors. A program-scoped rule only evaluates for episodes of that program. |
status | draft, active, paused, deprecated | Only active rules run. POST defaults to draft. |
trigger_type | see below | When the rule is evaluated. |
trigger_config | object | Per-trigger filter; shape depends on trigger_type. |
condition_expression | string | Must evaluate true for the actions to run. Validated on write. |
condition_params | object of number | Named thresholds available to the expression as {thresholds.name} or $name. |
actions | array | Executed in order when the condition matches. Each has a type. |
priority | small int | Evaluation order within a trigger (materialized rules use 50). |
cooldown_seconds | int | Minimum seconds between executions of this rule on the same episode. 0 disables. |
max_executions_per_episode | int or null | Hard cap per episode. |
Rules are environment-bound: a rule authored in the sandbox never evaluates a production observation.
Triggers
trigger_type | Fires on | trigger_config |
|---|---|---|
observation_create | A new observation linked to an active episode | biosignal_ids: [int], loinc_codes: [string] (empty means any), latency_classes: [string] (realtime, near_realtime, delayed, historical; empty means any), max_data_age_minutes: int or null, context_only: bool |
event_create | A new timeline event | event_types: [string] |
episode_start, episode_update | Episode lifecycle | none |
schedule | A cron grid, evaluated by the policy worker | cron: string, timezone: string (default UTC) |
inactivity | No data for a period, evaluated by the policy worker | threshold_hours: int |
manual | Never automatically | none |
An observation only reaches observation_create rules after it is linked to an active episode, and it is linked only when the episode's program lists that signal in required_signals. The public POST /api/v1/observations write links to the episode_ids you pass explicitly and does not evaluate rules; device paths (the cellular webhook, wearable syncs, the DSP pipeline) link and evaluate.
The condition grammar
The expression language is deliberately small:
- Comparisons:
>=,<=,>,<,==,!= - Logic:
AND,OR,NOT, parentheses - Arithmetic:
+,-,*,/ - Null checks:
state.start_ts != null - Variables: signal names (
hr,hrv_rmssd,spo2,systolic),state.<key>,baseline.<key> - Thresholds:
{thresholds.systolic_high}or$systolic_high, substituted fromcondition_paramsbefore parsing
On an observation_create trigger the reading is bound as follows: LOINC 8867-4 becomes hr, 80404-7 becomes hrv_rmssd, 59408-5 becomes spo2, any other coded scalar becomes observation_value, and each numeric component of a panel observation is bound under its own name (a blood-pressure panel exposes systolic and diastolic).
POST /api/v1/policies/{policy_id}/test evaluates the condition against signals, state and baselines you supply, with the rule's own condition_params as thresholds, and returns condition_matched without executing anything.
Actions
Each action is an object with a type and the fields below. Attention actions require an episode in context; without one they record success: false with "error": "No episode context" and the rule moves on.
type | Fields | What it does |
|---|---|---|
contribute_attention | points (1-100), signals: [string], reason, half_life_hours (default 2.0) | Appends a contribution to the episode's attention ledger, sourced to this rule's slug, then recomputes attention. This is the normal way a rule raises attention. |
set_attention_floor | value (0-100), reason | Sets the episode's floor. Attention cannot fall below the floor until a person lowers it by acknowledgment. |
clear_contribution | source_name, reason | Clears every active contribution on the episode whose source name matches (for example the slug of the rule that raised it), then recomputes. Use it for the end condition of the thing that raised attention. |
set_attention | value, reason | Legacy direct set. Still accepted; prefer contribute_attention. |
update_episode_status | status, reason | Changes the episode status. |
create_event | event_type, payload | Emits a timeline event sourced to the rule, which can in turn fire event_create rules and export triggers. |
enqueue_agent | agent_type (an agent slug or type), priority | Runs one of the program's agents against the episode. |
send_notification | channel, template, params | Renders the template and sends it through the communication gate. A held or withheld outcome is recorded and nothing is sent. |
create_observation | loinc_code or biosignal_id, value_expression, interpretation | Writes a derived observation whose value is the expression's result. |
start_symptom_session | protocol_ref | Opens an SMS symptom interview against the program's symptom_protocol, whose id must equal protocol_ref. Validated before opening; a missing or malformed protocol is a recorded failed action, never a session. |
Every evaluation that passes the trigger filter is recorded in the rule's execution history, matched or not: trigger_type, trigger_ref, condition_result, actions_executed, actions_failed, evaluation_duration_ms, executed_at. A rule in cooldown, or a reading outside the rule's trigger filter, is skipped before evaluation and leaves no execution row.
Rules the program materializes
Two kinds of rule are derived from a program's spec on every save, so the authored document and the running rule cannot drift:
- Each agent with an
on_scheduletrigger gets aschedulerule whose action isenqueue_agentfor that agent. - A
symptom_protocol.triggergets anobservation_createrule scoped to the program, filtered to the trigger'sbiosignal_slugs, with the trigger'sexpression,thresholdsascondition_params,cooldown_seconds,max_sessions_per_episodeas the execution cap,max_reading_age_minutesas the data-age guard, and a singlestart_symptom_sessionaction naming the protocol'sid.
Materialized rules appear in GET /api/v1/policies like any other and are re-synced when the program is saved.
Attention
Attention is computed, never stored as an opinion. The calculator reads the episode's active contributions, decays each one, sums the ones still above a minimum, multiplies for converging signals, caps at 100, and applies the floor:
current(c) = c.points × 0.5 ^ (hours_since(c.created_at) / c.half_life_hours)
base_sum = Σ current(c) for active c with current(c) ≥ min_contribution_threshold
attention = max(floor, min(100, base_sum × correlation_multiplier))correlation_multiplier is chosen by the largest signals list on any single active contribution: one signal 1.0, two signals 1.25, three or more 1.5.
Defaults, and where a program overrides them
All of these live in spec.attention_config; a program without the block uses the defaults.
| Setting | Default | Path |
|---|---|---|
| Default half-life | 2.0 hours | decay.default_half_life_hours |
| Minimum active contribution | 1.0 | decay.min_contribution_threshold |
| Multipliers | 1.0, 1.25, 1.5 | correlation_multipliers.single_signal, .two_signals, .three_plus_signals |
| Routine | 0 to 20 | thresholds.routine_max |
| Elevated | 21 to 40 | thresholds.elevated_max |
| Concerning | 41 to 60 | thresholds.concerning_max |
| Urgent | 61 to 80 | thresholds.urgent_max |
| Critical | 81 to 100 | above urgent_max |
| Acknowledgment required above | 60 | floor.require_acknowledgment_above |
| Auto-clear floor below | 0 (disabled) | floor.auto_clear_below |
| Classification labels to points | empty | classification_attention_map (image and model classifications) |
The floor and acknowledgment
A floor is set by a rule (set_attention_floor) and is the minimum attention until a person acknowledges. POST /api/v1/episodes/{id}/attention/acknowledge takes new_floor (must be less than or equal to the current floor), reason, and optional action_taken and notes; it writes an acknowledgment record with the previous floor and the attention at that moment, then lowers the floor and recomputes. A floor cannot be raised through acknowledgment.
Who else writes the ledger
- The symptom agent adds a contribution when a closed interview surfaces something a person should see; reviewing that agent run clears it.
- A care hand-off routed by a protocol rule adds a contribution sourced
care_handoff; resolving the hand-off clears it. - Every recompute that changes the level also emits an
attention_levelobservation on the episode, so attention over time is queryable like any other signal.
GET /api/v1/episodes/{id}/attention returns level, level_label, floor, base_sum, correlation_multiplier, active_contributions, top_reason, the per-contribution breakdown, and config_source (profile:<slug> or system_default). GET /api/v1/episodes?attention_gte=61 lists the episodes at or above a level.
Quickstart: from a reading to a routed follow-up
Create a program with one threshold rule, post a synthetic cuff reading through the device webhook, and watch the rule fire and attention rise - on the sandbox, no device required.
Symptom agent and the gate
What the SMS symptom agent may do, what it never does, the deterministic red-flag screen, the three gate outcomes and the hold, and the AI-identity disclosure.