Skip to content
RoutineOS
Esc
↑↓navigate↵open⌘Jpreview
On this page

Hybrid routines

The default kind. The model reads and judges; code applies every rule a person can edit, computes ids and writes the store. Where the line sits, how settings filters work, and Hiring Signals (jobs-watch) in full.

What it is

A hybrid routine splits the work. The model does what needs reading: search, fit, summaries, drafts. Code does what must give the same answer every run: settings filters, caps, ids, outcomes, learning and the store write. Most useful routines are hybrids. The piloted example is Hiring Signals, built from the jobs-watch template.

Like the other kinds, “hybrid” is a design pattern, not a field. In a pack it shows as a mix of model and code stages; in a prompt it shows as reading steps followed by routineos.py learn and routineos.py write. See Routine kinds.

Why it exists

Neither half works alone. A pure prompt cannot guarantee that a Desk edit to title_keywords changes the next run: the model may read the new value and still apply its own idea of a match. Pure code cannot tell a real commerce rebuild from a support role. A hybrid gets both, with a clear line between them.

The line is one rule: a rule a person edits in Settings is always code. If the model applied it, an edit on the Desk would not reliably change the run.

When to choose it

Choose a hybrid when:

  • The routine reads unstructured sources (web pages, mail, documents) and a person tunes what it keeps.
  • Results need stable ids across runs, so a role seen on Monday and Thursday is one record with two sightings.
  • The routine should learn from feedback and propose settings changes.

That covers almost every watch routine. Choose code instead when the input is already structured and there is nothing to read. Add a skill when one of the model’s steps needs long, shared guidance.

Use cases

Use case Model half Code half Shipped example
Watch job boards for hiring signals Search, read postings, summarise Market, keyword and exclusion filters, ids, learning jobs-watch (piloted)
Triage an inbox Label, summarise, draft Domain skips, caps, the write inbox-triage (unpiloted)
Weekly marketing report Collect numbers, audit pages Week-over-week change weekly-marketing-report (unpiloted)
Shortlist candidates Mark each against the rubric Score totals candidate-shortlist (unpiloted)

How it works

Who does what

For Hiring Signals:

Step Who Why
Read settings Model reads settings/<routine> Values only; it never writes settings/
Search and read postings Model Needs reading
Record roles and companies Model, into run.json Structured output
Apply markets, title_keywords, excluded_companies, excluded_title_words Code (write) A rule people edit is code
Ids, outcomes, counts Code (write) Stable across runs
Count signals and propose Code (learn) Thresholds and memory hold
Phrase Inbox messages Model Wording
Hold the intro note Gate Fixed mechanism
Write the store Code (write), applied as ArtifactData batches One contract

Settings filters

A setting becomes a filter in definition.json. Each type lists settingFilters, pairing a setting with the record field it tests. From templates/jobs-watch/definition.json:

Type Setting Field Role
company excluded_companies domain exclude
role markets market include
role title_keywords title include
role excluded_title_words title exclude
role excluded_companies company_domain exclude

The role (include or exclude) comes from the setting’s role in routine.toml. write applies them in scripts/desk_contract.py:

  • An include setting with values drops a record whose field matches none of them: not in title_keywords.
  • An exclude setting drops a record whose field matches any of them: excluded by excluded_companies.
  • An empty setting, or an empty field, filters nothing.
  • A text field (such as title) matches a case-insensitive substring, so the keyword commerce matches “Ecommerce Developer”. Other field types (choice, url) match whole values, case-insensitive for text. The substring rule came from a pilot run, 2026-10-29-0438, in which whole-word matching dropped two Ecommerce roles (references/write-contract.md).

A dropped record is still written, with status dropped and the reason. A person who marks it “keep” on the Desk overrides the filter while that decision stands, and a person’s “drop” wins over the filters the same way.

Report everything, let code drop

The model is told to report every posting it read, including ones the settings will drop. From the jobs-watch prompt, step 2: “Report every posting you read, including ones the settings will drop; the write contract marks those dropped with a reason.” People can then see what the settings cut and keep the ones they want, and learn can count how often a setting cuts something a person kept.

Learn, then append, then write

learn turns feedback on earlier runs into learning.json and proposals.json. The model puts the proposals at the front of the run’s inbox unchanged (it may reword title and why only), and then write applies the run. The order is fixed: proposals appended after the write are lost. The model never writes a per-item proposal itself; write refuses one unless this run’s learn raised it.

Worked example

Hiring Signals tracks public job postings for commerce and automation roles, links each role to its company, and puts the few that need a person in the Inbox. It is built from jobs-watch, the one piloted template. It never sends, posts or deletes.

The routine

From templates/jobs-watch/routine.toml:

Field Value
type watch
schedule 0 22 * * 1,4 (22:00 UTC Monday and Thursday)
first_run report-only
Store artifact-db
Gate email, 48-hour timeout

Settings, all editable on the Desk:

Setting Default Notes
markets AU, US, CA, NZ, PH Include; each market adds at least one search
title_keywords Shopify, headless, commerce, AI automation Include
excluded_companies empty Exclude, by domain
excluded_title_words empty Exclude, by title
lookback_days 7 1 to 30
max_inbox_per_run 5 0 to 20

The stages

From the jobs-watch pack, site/templates/jobs-watch.json:

Stage Kind Does
search model Searches each market for postings inside the lookback window whose title matches a keyword, and opens the ones it reports (web-search, web)
record model Records each role linked to its company and drafts the Inbox messages, inside max_inbox_per_run
learn code python3 skills/routineos/scripts/routineos.py learn --docs snapshot.json --run run.json --out learn
write code python3 skills/routineos/scripts/routineos.py write --docs snapshot.json --run run.json --out batches; writes runs, entities, inbox and pending
approve gate A person approves or declines an intro note by email; the gate times out after 48 hours. Nothing is sent from this routine (mail)

Connectors: the built-in web-search and web tools (read, required) and a mail connector (write, optional, alternative Slack) for the gate.

Build it

python3 skills/routineos/scripts/routineos.py pack install site/templates/jobs-watch.json --dir hiring-signals --name hiring-signals

jobs-watch has no lib/ or skill files, so new hiring-signals --from-template jobs-watch gives the same routine files without pack.json.

The run

The prompt has seven steps:

  1. The model searches each market for postings inside the lookback window whose title matches a keyword.
  2. It records a role (title, company, company_domain, market, location, remote, salary_max, posted, url, seniority, summary) linked to a company (name, domain, industry, size, country), for every posting it read. summary is 1 to 2 factual sentences, at most 400 characters, with no inference.
  3. It writes Inbox messages only for roles a person should look at today: match, alert or proposal.
  4. An intro note becomes an Inbox message of kind approval with a gate. Nothing is sent.
  5. It builds the run input per references/write-contract.md, with status partial if a market failed to load.
  6. learn writes learning.json and proposals.json; the model puts the proposals at the front of inbox.
  7. write applies the filters and writes five batches: started, entities, summary, inbox, done.

The settings line at work

Suppose a run reads three postings:

Title Market Company domain Result
Senior Ecommerce Developer AU shop.example Active: “commerce” is a substring of the title
Shopify Support Agent US help.example Dropped, excluded by excluded_title_words, once a person adds “Support”
Hydrogen Engineer NZ build.example Dropped, not in title_keywords, until a person adds “hydrogen”

When a person accepts “Add hydrogen to title_keywords” on the Desk, the next run’s write treats “Hydrogen Engineer” as a match with no prompt change. When a person marks the support role “keep”, it stays active despite the filter. Neither change touches the prompt or the model’s judgement.

Common pitfalls

  • Telling the model to skip postings the settings would drop. Then nobody sees the drops and learning has nothing to count.
  • The model applying a keyword itself. The model’s idea of a match and the code’s can differ; in a pilot they disagreed over “Ecommerce” and commerce. Let the code decide.
  • A setting with no filter. A setting in routine.toml that no settingFilters entry names is only advice to the model. If a person expects it to hold, bind it to a field.
  • Learning after write. Proposals appended after the done batch are lost. The order is learn, then append, then write.
  • The model writing settings/. It proposes a change with {"setting", "from", "to"}; the Desk writes to when a person accepts.
  • Expecting new --from-template to copy scripts or skills. It copies routine.toml, prompt.md, README.md and definition.json. That is enough for jobs-watch, not for templates with lib/ or skills/.

Was this page helpful?