---
title: Quickstart
description: 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.
sidebar:
  order: 5
---

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](/introduction/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](/reference/cli#permissions).

## 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`:

```html
<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](/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:

```json
{
  "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:

```json
"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 (`domain` for companies, `field:url` for roles), linked each role to its company, applied the `title_keywords` filter, 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](/guides/first-routine): the same routine taken through verify and deploy.
- [Learning](/concepts/learning): what happens when people keep the roles the settings dropped.
- [The store and the write contract](/concepts/store-and-write-contract)
