Quickstart
Build the Hiring Signals routine from the jobs-watch template, lint it, seed its Desk, and plan a first run's writes on your own machine.
This walk-through builds Hiring Signals, a hybrid routine that watches public job boards for commerce roles. You will scaffold it from the jobs-watch template, lint it, read its permission screen, seed its Desk, and plan the store writes of a sample run, all locally. Every output below is what the CLI printed when these commands were run.
You need the plugin installed and Python 3.11 or later. See Installation.
To keep commands short, set a variable for the CLI path (adjust it to where your checkout or plugin lives):
RO=skills/routineos/scripts/routineos.py
1. Scaffold the routine
python3 $RO new hiring-signals --from-template jobs-watch --app
created hiring-signals
--from-template jobs-watch copies templates/jobs-watch/ and renames the routine to hiring-signals. --app adds the app layer: app.toml, risks.md, CHANGELOG.md and lib/. The folder now holds:
| File | What it is |
|---|---|
routine.toml |
Type, schedule, store, settings and the gate |
prompt.md |
The routine prompt, with the untrusted-data clause and the push rule |
definition.json |
Record types (company, role), identity rules, setting filters, views and learning |
dashboard/index.html |
The Desk page, ready to publish |
app.toml |
The app manifest: routines, store, budget, integrations, environment |
risks.md |
Known risks and what the routine relies on |
CHANGELOG.md |
Where improvements are recorded |
lib/budget.py |
The run budget helper |
README.md |
What the routine does |
The routine name becomes the producer id. Every document this routine writes carries producer: "hiring-signals", and its settings live at settings/hiring-signals.
2. Lint it
python3 $RO lint hiring-signals
ok: no problems
Lint checks the routine type, the schedule (at least one hour apart), every setting’s shape, that a gate exists before any send action, and that the prompt carries the untrusted-data clause and a Push rule: line. With a definition, it also checks the record types, views and learning block.
3. Read the permission screen
python3 $RO permissions hiring-signals
The screen lists everything the routine will touch before anything is created. An excerpt:
Permission screen: hiring-signals
Schedule
- hiring-signals: UTC cron 0 22 * * 1,4, 1 run a day
- Total: 1 run a day against your cap of 1 run a day.
- The shortest allowed schedule interval is 1 hour.
Store
- Kind: artifact-db
First run
- The first run is report-only: it reads and reports what it would do, and nothing is sent or written outside the store.
Nothing is created until you say yes. A change to any line above makes a new screen.
The full screen also covers apps and hosts, connectors, tools, delivery, environment and secrets, budget, and what is not yet checked. See CLI reference.
4. Seed the Desk
python3 $RO seed hiring-signals --out hiring-signals/seed
ok: wrote 1 batch file(s) to hiring-signals/seed; lint passed. Apply them in order with ArtifactData batch.
hiring-signals/seed/batch-001.json has phase seed and four writes:
| Document | Holds |
|---|---|
meta/workspace |
Workspace name, producers, template and lint result |
meta/definition |
The compiled definition the Desk and the write step read |
settings/hiring-signals |
The setting defaults from routine.toml |
settings/workspace |
Workspace settings: merge policy per type, notifications inbox, empty quiet hours |
Seed writes settings only when they are absent, so reseeding never overwrites a value a person saved.
5. Publish the Desk
Ask Claude to publish hiring-signals/dashboard/index.html as a private artifact with the capabilities {"db": {}, "user": {}, "downloads": true}, then to apply seed/batch-001.json with one ArtifactData batch call. db is required; user turns on per-person state (read, done, snooze); downloads turns on file export. The page reads its store from a config block already filled in by new:
<script type="application/json" id="desk-config">{"store": "db", "workspace": "hiring-signals"}</script>
Open the artifact. The Desk shows the first-run state: settings are in place and no run has written yet. See The Desk.
6. Plan a run’s writes
A deployed run does the searching and reading. You can see what the write contract does with its output without deploying anything. Save this as run.json; it is the run input a run builds in step 5 of its prompt:
{
"producer": "hiring-signals",
"runId": "2026-11-02-2200",
"startedAt": "2026-11-02T22:00:04Z",
"endedAt": "2026-11-02T22:09:41Z",
"claimed": 3,
"parsed": 3,
"records": [
{"type": "company", "values": {"name": "Northwind Goods", "domain": "northwind.example", "industry": "homewares", "country": "AU"}},
{"type": "role", "values": {"title": "Senior Shopify Engineer", "company": "Northwind Goods", "company_domain": "northwind.example", "market": "AU", "posted": "2026-11-01", "url": "https://jobs.example/au/1001", "seniority": "senior"}, "links": [{"type": "company", "key": "northwind.example"}]},
{"type": "role", "values": {"title": "Graduate Data Analyst", "company": "Northwind Goods", "company_domain": "northwind.example", "market": "AU", "posted": "2026-10-31", "url": "https://jobs.example/au/1002"}, "links": [{"type": "company", "key": "northwind.example"}]}
],
"inbox": [
{"kind": "match", "title": "Northwind Goods is hiring a Senior Shopify Engineer", "why": "Senior commerce role in AU, posted yesterday.", "refs": [{"type": "role", "key": "https://jobs.example/au/1001"}]}
]
}
The write step needs a snapshot of the store as {path: {version, data}}. A deployed run reads it from the artifact database. Locally, build it from the seed batch:
python3 - <<'EOF'
import json
b = json.load(open("hiring-signals/seed/batch-001.json"))
snap = {f"{w['collection']}/{w['doc_id']}": {"version": 1, "data": w["data"]} for w in b["writes"]}
json.dump(snap, open("snapshot.json", "w"))
EOF
Run the learning step, then the write step, in that order:
python3 $RO learn --docs snapshot.json --run run.json --out learn
python3 $RO write --docs snapshot.json --run run.json --out out
ok: 0 proposal(s) in proposals.json; apply learning.json as one ArtifactData batch
ok: wrote 5 batch file(s) to out
No proposals yet: nobody has made a decision for learning to count. The write step planned five batches, one per phase:
| Batch | Phase | Writes |
|---|---|---|
batch-001.json |
started |
runs/2026-11-02-2200 |
batch-002.json |
entities |
entities/company--northwind-example, entities/role--https-jobs-example-au-1001, entities/role--https-jobs-example-au-1002 |
batch-003.json |
summary |
runs/2026-11-02-2200 |
batch-004.json |
inbox |
inbox/2026-11-02-2200--1 |
batch-005.json |
done |
runs/2026-11-02-2200 |
Look at the run summary in batch-003.json. The graduate role does not contain any of the title keywords, so code dropped it and said why:
"counts": {"dropped": 1, "new": 3, "unchanged": 0, "updated": 0},
"dropped": [{"id": "role--https-jobs-example-au-1002", "reason": "not in title_keywords"}],
"droppedByReason": {"not in title_keywords": 1},
"inboxWritten": 1,
"status": "ok"
The dropped role is still written, with status: "dropped" and dropReason: "not in title_keywords", so a person can see what the settings cut and keep it if the settings were wrong.
What just happened
- The model’s job (in a real run) was to find three postings, read them, and judge that one is a strong match.
- Code’s job was everything else: it minted ids from the identity rules (
domainfor companies,field:urlfor roles), linked each role to its company, applied thetitle_keywordsfilter, counted, capped the Inbox, and planned the writes in order. - Nothing was written to
settings/,decisions/or any other people-owned path. The write contract refuses those paths.
Next
- Build your first routine: the same routine taken through verify and deploy.
- Learning: what happens when people keep the roles the settings dropped.
- The store and the write contract