7 Routines
A routine is an automation that runs on the Hub without a model, such as watering a plant when its soil gets dry or at 07:00. Every write and action a routine makes goes through the Safety Kernel like any other command.
A routine is YAML or Python. YAML routines are data. Python routines are code, so only a human may enable them.
This version exposes hal/routines/list and hal/routines/dryRun. Drafting, enabling and running routines are MCP and REST only.
7.1 YAML routines
id: water-when-dry
description: Water for 20 s when the soil is drier than 40 %.
triggers:
- when: "plant-waterer.soil_moisture < 40 %"
steps:
- action: { device: plant-waterer, action: water, args: { duration: { value: 20, unit: s } } }This is the routine openhal demo --full runs (docs/demo/e2e.yaml). An agent drafts it, a person enables it, and it waters once each time the soil turns dry.
A YAML routine is parsed with the same restricted loader as a Device File (Device files) and MUST be at most 128 KiB.
| Key | Content |
|---|---|
id | A slug matching ^[a-z][a-z0-9-]{0,47}$. It MUST match the name the routine is stored under |
description | Optional, at most 500 characters |
triggers | 1 to 20 triggers. Default [{ manual: true }] |
steps | At least one step |
on_block | stop (default) ends the run when a command is not done. continue goes on to the next step |
max_runtime | A duration. Default 10 min. The Hub's own cap still applies |
7.1.1 Triggers
| Trigger | Fires |
|---|---|
every: <duration> | When at least that long has passed since the last start. At least 1 s |
at: "HH:MM" | Once a day at that local time |
when: <expression> | When the expression becomes true, on the change from not true to true |
manual: true | Only when a human runs it |
7.1.2 Steps
| Step | Shape |
|---|---|
write | { device, channel, value }. Numeric values MUST carry a unit |
action | { device, action, args? }. Numeric arguments MUST carry a unit |
wait | A duration of at most 1 h |
log | Text of at most 300 characters, recorded in the run |
if | { if: <expression>, then: [steps], else: [steps] } |
- A routine MUST have at most 200 steps in total, counting nested ones.
ifsteps nest at most 4 levels. - Expressions in routines use the language of Device files, but every reference MUST name its Device as
device_id.channel. A bare word is an enum value, never a channel. - An
ifwhose expression is unknown takes theelsebranch.
7.2 Python routines
TRIGGERS = [{"when": "plant-waterer.soil_moisture < 40 %"}]
async def run(ctx):
await ctx.action("plant-waterer", "water", duration={"value": 20, "unit": "s"})- A Python routine MUST define
async def run(ctx). It MAY defineTRIGGERSas a literal list of trigger mappings. Without it, the routine is manual. ctxoffersread(device, channel),write(device, channel, value, unit),action(device, action, **args)with numbers as{"value": 2, "unit": "s"},wait(duration)of at most 1 h,value(expression)andlog(message).- Every device operation goes through
ctx, andctxgoes through the Safety Kernel. A blocked command ends the run. - Python routines run in the Hub process as trusted code. The Hub does not sandbox them (Security).
7.3 Enabling
- A new routine, from any author, MUST start disabled.
- Changing a routine's content MUST disable it.
- A human MAY enable any routine. A human reads the dry-run result before enabling.
- An agent MAY enable a routine only when the Hub setting
routines.agent_enable_low_hazardis on (default off), the routine is YAML, every Device it touches is low hazard, and the agent's scope is at leastcontrol_low. - A routine whose Devices aren't all registered, or that failed validation, MUST NOT be enabled. Nothing may be enabled while the Hub is stopped.
- Agents with scope
readMUST NOT draft routines. Routines MUST NOT create routines. Only a human may delete one or run one by hand.
7.4 Running
- A routine runs as Principal
routine:<id>with the scope of whoever enabled it, capped atcontrol_medium, and with the SHA-256 of its canonical content. Approvals for high hazard Devices are bound to that hash. - YAML content is hashed after parsing, so comments don't change the hash.
- A routine runs at most once at a time, and starts no more often than
routines.min_interval_s(default 1 s). - A run is cancelled after the smaller of its
max_runtimeand the Hub'sroutines.max_runtime_s(default 1 h). - A routine cannot wait for a person. A command that needs confirmation is blocked (Safety).
- A run ends
ok,blocked,failedorcancelled. Every run and every command in it is logged. - A stop MUST cancel running routines and disable every enabled routine. A human enables them again.
7.5 Dry run
A dry run evaluates every command in every branch, in order, as the routine would send it. Nothing is sent to any Device.
- A
waitadvances the simulated clock, so rate limits are judged as in a real run. - Each branch of an
ifstarts from a copy of the state before it. - The result is
{ ok, decisions, problems }.okistrueonly when every Decision isallowedand there are no problems. - A Python routine can't be dry-run step by step. Its dry run reports that as a problem.