---
title: Move an n8n workflow to a routine
description: Map a scheduled automation workflow onto a RoutineOS routine. Trigger to schedule, nodes to stages, the sheet to the store, the send to a gate, and what the Desk adds. Worked through for Hiring Signals.
sidebar:
  order: 2
---

Many teams already run a scheduled job as an n8n (or Zapier or Make) workflow: fetch something, ask a model about it, filter, append to a sheet, send an email. That is the same shape as a watch routine. This guide maps one onto the other, and says what changes on the way.

This guide rebuilds the workflow by hand as a routine folder, using the workflow as the spec. To convert an n8n export, URL or gallery template into a pack instead, use `routineos migrate n8n`; see [Migrate from n8n](/guides/migrate/n8n). The two meet: read this guide to judge what the converter wrote.

## Why move it

What a routine adds over a workflow of the same shape:

| In the workflow | In the routine |
|---|---|
| A row per item in a sheet, appended each run | One record per item, keyed by an identity rule, with every sighting kept |
| A filter node that silently drops items | Settings filters that keep dropped items with a reason people can see and overrule |
| Settings edited inside the workflow | Settings on the Desk, with a cost note each |
| A send node that fires every run | A gate: the send waits for a named person's approval |
| No memory of what people did | Learning: people's keeps and drops become suggested setting changes |
| One builder edits it | People use the Desk; the builder owns the definition |

What it costs: a routine is a scheduled Claude session, not a fixed graph. Model steps can vary from run to run, which is why RoutineOS puts identity, filters, counting and writes in code. See [Stages](/concepts/stages).

## The mapping

| Workflow part | Routine part | Where |
|---|---|---|
| Schedule trigger | `schedule`, five-field UTC cron, at least one hour apart | `routine.toml` and `app.toml` `[[routine]]` |
| Webhook or app-event trigger | No direct equivalent in a watch routine. The scheduled tasks docs list API and GitHub triggers; see [Scheduling](/infrastructure/scheduling) | |
| HTTP request or scraper node | A model stage that searches or fetches, or a code stage for a fixed API | `prompt.md`, `lib/` |
| AI node | A model stage | `prompt.md` |
| IF or filter node | A list setting with a role (`include` or `exclude`), bound to a field in `settingFilters` | `routine.toml`, `definition.json` |
| Set or transform node | Record fields in the definition | `definition.json` |
| Sheet or database append | Records in the store, written by `routineos.py write` | `[store] adapter = "artifact-db"` |
| Email or chat send node | An `[[action]]` of kind `send` behind a `[gate]`, and a write `[[integration]]` with `gate = true` | `routine.toml`, `app.toml` |
| Credentials | A connector, or a network secret for one host. Never a key in the prompt or files | `app.toml` `[[integration]]`, `[[secret]]` |
| Error workflow | `state/health`, the run record, and the budget alert | Prompt header, `[budget] alert_channel` |

## Worked example: Hiring Signals

Suppose Hiring Signals runs today as this workflow (illustrative; your node names will differ):

1. Schedule trigger, Monday and Thursday at 22:00 UTC.
2. One HTTP request per job board and market.
3. An AI node that extracts title, company, market and URL, and says why a posting matters.
4. A filter node: keep titles containing Shopify, headless, commerce or AI automation.
5. A sheet node: append each kept posting as a row.
6. An email node: send the best match to the sales lead.

Rebuild it from the template that already has this shape:

```
python3 skills/routineos/scripts/routineos.py new hiring-signals --from-template jobs-watch --app
```

```
created hiring-signals
```

Now map each node.

**1. Schedule.** The template's `routine.toml` already has it:

```toml
[routine]
schedule = "0 22 * * 1,4"  # UTC, Monday and Thursday; minimum interval is 1 hour
first_run = "report-only"
```

`first_run = "report-only"` has no workflow equivalent. The routine does nothing outside the store until a person has checked a run.

**2 and 3. Fetch and extract.** These become model stages. The jobs-watch pack (since 0.6.0) names them `search` and `record`. The prompt tells the run what to search and which fields to return. The fields are declared once, in `definition.json`, for the `role` type (`title`, `company`, `company_domain`, `market`, `location`, `remote`, `salary_max`, `posted`, `url`, `seniority`, `summary`) and the `company` type.

**4. Filter.** The filter node's keyword list becomes a setting:

```toml
[[setting]]
name = "title_keywords"
type = "list"
role = "include"
label = "Title keywords"
default = ["Shopify", "headless", "commerce", "AI automation"]
cost_note = "each keyword adds a search per market"
```

and the definition binds it to the field:

```json
"settingFilters": [{"setting": "title_keywords", "field": "title"}]
```

The difference: the workflow threw non-matching rows away. The routine writes them with `status: "dropped"` and `dropReason: "not in title_keywords"`. Matching is a case-insensitive substring, so `commerce` matches "Ecommerce Developer".

**5. Sheet.** Rows become records. The role's identity is `field:url`, so the same posting seen on Thursday updates Monday's record and adds a sighting, instead of adding a second row. Companies use identity `domain`, and each role links to its company.

**6. Email.** The send moves behind a gate:

```toml
[gate]
enabled = true
transport = "email"
timeout_hours = 48
```

The run drafts the note as an Inbox message of kind `approval`, emails the approver a code, and stops. A later run reads the reply. Only `approve <code>` from the named approver counts. Declare the mail connector in `app.toml` as a write integration with `gate = true`; see [Build your first routine](/guides/first-routine#2-fill-in-apptoml).

As shipped, the 48-hour gate is shorter than the 72 and 96 hour gaps between Monday and Thursday runs, so a gate expires before the next run reads it. Raise `timeout_hours` above 96 before switching gated messages on. See [Notification channels](/infrastructure/notification-channels#worked-example).

**What you gain on the Desk.** The sales lead sees new roles on Today, browses dropped ones under "Dropped by settings", keeps the ones the keywords missed, and approves or rejects intro notes. After enough keeps, learning suggests the missing keyword. See [Learning](/concepts/learning).

## A second shape: a weekly report

n8n's public template "AI marketing report (GA, Google Ads, Meta Ads)" ([n8n.io/workflows/2783](https://n8n.io/workflows/2783)) pulls seven days of marketing data on a schedule, has a model write a report, and sends it by email or Telegram, per its listing (fetched 2026-10-30). As a routine:

- Schedule: weekly cron.
- Fetch: connectors for the ad and analytics accounts, if they exist on your claude.ai account. Check the connectors directory; none is assumed here.
- Records: one `report` record per week, identity on the week, so history accumulates.
- Send: the report email is a notice to the team, not an action on someone else's behalf. Recommendations that change spend go behind a gate.

There is no template for this today. A `weekly-marketing-report` pack is a candidate; see [Templates](/templates).

## Common pitfalls

- **Porting the trigger as-is.** A webhook that fires per event has no equivalent in a scheduled watch routine. Batch it.
- **Copying the filter as a prompt instruction.** Put it in a setting with a role, so code applies it and people can see and change it.
- **Keeping the credential.** An API key from the workflow does not go into the routine. Use a connector, or a network secret scoped to one host.
- **Sending on the first run.** First runs are report-only. Sends need a gate and a promotion step with its own permission screen.
- **A gate shorter than the schedule gap.** The gate is read only on the next run.

## Related

- [Routines](/concepts/routines)
- [Settings](/concepts/settings)
- [Gates and approvals](/concepts/gates-and-approvals)
- [Connectors and MCP](/infrastructure/connectors-and-mcp)
- [Migrate to RoutineOS](/guides/migrate)
- [Migrate from n8n](/guides/migrate/n8n)
