Pimpo
Documentation

Routines#

A routine is a small JavaScript program that Pimpo runs for you: on a schedule, when something new shows up, when another service calls it, or when you answer one of its questions. You don't write it. You ask for something once, the agent does it while you watch, and Pimpo turns what happened into code that has tests and a fixed list of what it may touch. After that the routine runs without a language model. The only exception is a routine that needs a yes-or-no judgment or a short text, which it gets from a small model. This guide is for owners who make routines by asking, and for developers who write, review or share them by hand.

See also User guide, Configuration, Writing a connector, Extending Pimpo, Threat model and the gallery.

Lifecycle#

StepWhat happensWhere you see it
AskYou describe the task in chat, with New task, or on a channel such as Telegram.Talk, Home, Routines
ExploreAn agent does the task once over MCP. Reads are real. Changes are only simulated.The exploration page: Working on it now, while you watch
CompileYou tap Turn into a routine. The compiler writes code, a manifest and tests, then checks them.Writing the routine and testing it…
RunThe routine runs on its trigger, without a model.Routines, the routine's page, Routines › Runs
FailA failed run stops the routine and tells you.Needs you, a notice with Run again and Redo with the agent
RepairThe agent explores the task again. Approving the result saves a new version.The routine's History tab

Explore#

The agent (internal/explore) runs with a limit of 15 minutes and 40 turns. Its model budget per exploration is $1 at most, or what is left of the daily limit if that is less, with a floor of $0.05. It sees Pimpo's capabilities as MCP tools, named with _ instead of . (gmail_search, telegram_send).

The exploration runs as a dry run:

Every subjective decision the agent makes ("is this email important?") must be recorded with its decide tool, one item at a time. The page shows each one as Decided “…” · N yes, N no. The routine can only repeat the decisions that were recorded.

When the exploration ends, you get its summary and two buttons: Turn into a routine and Discard. There is nothing to compile in two cases. If every read failed, a routine would only repeat the error. If the agent only set a one-time reminder (reminder.set), the reminder is already scheduled and nothing needs to repeat.

Compile#

The compiler (internal/compiler) sends the recorded trace and the capability catalog to the model for writing routines. It asks for one JSON object with name, description, manifest, code and tests. The attempt is then checked:

  1. The manifest is valid, and something starts the routine: a schedule, a watch or a webhook.
  2. Every judgment the manifest declares was made during the exploration.
  3. The routine passes a replay of the exploration: the recorded reads, as canned answers, must lead to the same writes (derived from what the agent did: the same count, mentioning the same titles, subjects and names).
  4. The routine passes its own tests.
  5. The code does not hard-code data from the recording. A string literal that copies a recorded value of 10 or more characters is rejected, unless the value was part of what you asked.

If an attempt fails, its problems go back to the model and it tries again, up to 3 attempts in the app. If no attempt passes, the exploration stays ready and shows the error, and the best candidate is kept.

Active, paused, stopped#

A saved routine has one of three states:

StateUI labelRuns?
activeActiveYes
pausedPausedNo. Webhook calls get 409.
brokenNeeds attentionNo, until you run, resume or repair it

Pause and Resume on the routine's page switch between active and paused. Run now runs the routine once. A paused routine stays paused; a routine stopped after a failure (Needs attention) goes back on its schedule.

Versions#

Every save (compile, repair, repository update, gallery update, import) creates a new version with a reason, such as "compiled from exploration …" or "repaired from exploration …". The History tab lists them and shows a code diff against the previous version. There is no button to go back to an older version. To restore one, put its files back in the repository folder and apply them (see Importing).

Triggers#

A manifest needs at least one of schedule, watch or webhook. Otherwise the routine is not saved. Every run receives a read-only global event.

TriggerStarts wheneventRecorded trigger
ScheduleThe cron expression fires{items: []}schedule
Catch-upPimpo starts after a missed run{items: []}catch-up
ManualYou tap Run now{items: []}owner
WatchA poll finds items not seen before{items: [...]}event
WebhookAnother service calls the routine's address{webhook: {...}}webhook
AnswerYou answer a question the routine asked{answer: {...}}answer

A routine never runs twice at the same time. A second start while one run is still going fails with "already running".

Schedule#

manifest.schedule is a standard 5-field cron expression (minute, hour, day of month, month, day of week), read by robfig/cron in the owner's time zone. The scheduler also accepts descriptors such as @daily in the manifest. A schedule you set in Routine settings must be 5 fields, and it cannot fire more often than every 5 minutes. There is no RRULE support.

When Pimpo starts, it checks each active routine's last run. A run missed less than 12 hours ago runs once, late. Older misses are recorded as skipped and are not replayed.

Watch#

A watch runs the routine when a read capability returns something new. No model is used for the checks.

"schedule": "",
"watch": {
  "capability": "gmail.search",
  "args": {"query": "from:{{sender}}", "unread": true},
  "key": "id",
  "every": "10m"
}

The first poll only learns what is already there. After that, items whose key was not seen before are passed as event.items, each shaped like that capability's results. Pimpo remembers the last 2,000 keys per routine. The routine should work on event.items and not call the watched capability again. A routine can have both a schedule and a watch.

Webhooks#

Set "webhook": true in the manifest (with schedule: "" and no watch) for a routine that another service starts. Any routine can also get a webhook: on its page, turn on Start by webhook.

What the routine receives:

{
  "webhook": {
    "method": "POST",
    "query": {"source": "shortcut"},
    "body": {"order_id": "1107", "total": 54.5},
    "received": "2026-10-08T10:00:00-03:00"
  }
}

body is the parsed JSON when the body is JSON. Otherwise it is the form fields as a string map, when the content type is a form. Otherwise it is the raw text. Repeated query or form values are joined with commas. A routine made for webhooks should do nothing when event.webhook is missing, for example on a manual run.

Questions and answers#

ask.owner({question, options, key}) sends you a question. The options appear as buttons in Needs you, on Telegram and on WhatsApp (a list when there are more than three), and numbered on channels without buttons. A typed answer is checked against the options, so event.answer.choice is always one of them. It returns {asked: "<id>"} at once, without waiting.

Your answer runs the same routine again (unless it is paused: then the answer is recorded, and you are told the routine will not act on it) with:

{"answer": {"key": "workout", "question": "Did you work out today?", "choice": "Yes", "index": 0, "asked": "2026-09-29T21:00:00-03:00"}}

The usual shape is:

async function run() {
  if (event.answer) {
    // record event.answer.choice in state, reply with notify.send if useful
    return
  }
  await ask.owner({question: "Did you work out today?", options: ["Yes", "No"], key: "workout"})
}

Only the person the question was for, or the owner, can answer it.

Reminders#

A one-time request ("remind me tomorrow at 9 to…") does not become a routine. The exploration calls reminder.set and ends without offering to compile. A routine can also call reminder.set({at, text}) or reminder.set({in, text}) (risk notify), along with reminder.list() and reminder.cancel({id}). Pending reminders are listed under Reminders at the top of Routines.

The routine format#

A routine is one JSON object (internal/routine.Routine):

{
  "name": "Exchange rate of the day",
  "description": "The rate in reais of the currency you choose, on weekdays.",
  "manifest": { },
  "code": "async function run() { … }",
  "tests": [ ]
}

In a repository folder, the same routine is split into three files. See Writing by hand.

Manifest#

FieldTypeMeaning
schedulestring5-field cron expression, or "" for a routine started by a watch or a webhook
capabilities[string]Everything the code may call. Required, at least one. Scoped capabilities name their host: http.getJSON:api.open-meteo.com, web.read:www.kabum.com.br.
judgments{name: question}Yes-or-no questions about one item, answered by the judgment model. Names must be JavaScript identifiers.
writes{name: instruction}Short texts a small model composes per item. Each instruction is up to 500 characters.
localestringLanguage of the messages: pt-BR, en-US, es-ES, fr-FR, de-DE, it-IT, ja-JP, zh-CN, ko-KR, ru-RU. It drives dates.format and money.format. It defaults to Portuguese.
params[Param]Settings the owner changes without code (below)
watch{capability, args, key, every}What to watch (see Watch)
webhookboolStarted by its webhook (see Webhooks)
uses[string]Ids of other routines this one runs with routines.run. Everything they touch must also be in capabilities.

Validate refuses a manifest in these cases: an unknown capability, a missing or unexpected scope, a judgment or write name that is not an identifier, a write instruction that is empty or over 500 characters, a watch on a non-read or undeclared capability, a watch without key, a bad every, an empty uses id, or a parameter that is declared twice or invalid.

The capability catalog is in internal/capability, plus whatever connectors register (see Writing a connector). Each capability has a risk: read, notify (a message to the owner only), reversible or irreversible. The routine page's What it can do tab lists them with their risk.

Params#

{"name": "currency", "label": "Currency", "type": "select", "options": ["USD", "EUR", "GBP"], "default": "USD", "help": "…"}
TypeValue the code sees
textstring
numbernumber (a string like "5,40" is accepted and converted)
booleantrue or false
date"2026-10-05"
time"07:30" (24-hour)
emaila bare address
selectone of options (required)
multiselecta list, each one of options (required)
location{name, latitude, longitude, timezone?, country?}
destinationsa list of destination ids. It also sets where notify.send and audio.send deliver for the run.

Every parameter needs a default, or a value set by the owner. A missing one fails the run with "<label> is not set". Unknown names are refused. Messages to the owner should use notify.send with a parameter {"name": "destinos", "label": "Where to notify", "type": "destinations", "default": []}. With no destination chosen, messages go to the owner's usual channel.

The JavaScript API#

The code is plain JavaScript (ES2020, run by goja) that defines async function run(). There are no modules, import, require, fetch, timers, network, disk or Intl. toLocaleString and its relatives are not available for formatting either. The routine sees only the globals below.

Capabilities#

Each declared capability becomes a method on a global object named after its prefix. gmail.search becomes gmail.search(...), and http.getJSON becomes http.getJSON(...).

const mails = await gmail.search({query: "from:bank", days: 7})
const data  = await http.getJSON("https://api.open-meteo.com/v1/forecast?latitude=" + params.city.latitude + "&longitude=" + params.city.longitude)
await notify.send({text: "…"})

Waking a company member#

A routine given to a company member (see the user guide's Companies of agents) runs as that member: its company's rules apply on top of the house's, and it is held while the member, its department or the company is paused. It can hand the member's agent work with company.wake (risk notify): the routine watches without a model, and the agent works only when there is something new.

const orders = await shop.orders({status: "new"})
if (orders.length) await company.wake({task: "Answer these customers about their delivery", items: orders})

items reach the agent as data, never as instructions, up to 16 KB. The call returns {queued: "<work id>"} at once; the agent works when the member is free and on duty. Called from a routine that is not a member's, it fails.

Widgets#

A routine that keeps something to look at calls widget.show (risk notify). Each call replaces the routine's widget with the same key (default main), so a routine can keep several.

await widget.show({kind: "metric", title: "Dollar", value: 5.18, unit: "BRL", trend: -0.8})
await widget.show({key: "orders", kind: "chart", chart: "area", title: "Orders", series: [{name: "Orders", points: [{label: "Mon", y: 18}, {label: "Tue", y: 24}]}]})
await widget.show({key: "stock", kind: "table", title: "Low stock", columns: ["Product", "Left"], rows: [["Mug", 4], ["T-shirt", 7]]})
KindFields
metricvalue, unit (a currency code such as BRL shows as money, % as a percent), trend (percent), subtitle
progressvalue, goal, unit
statusstatus (ok, warn, alert), value, text, items
texttext
listitems: [{title, detail, badge, value, status, link}]
tablecolumns, rows: [[...]]
chartchart (line, area, bar, donut), and values: [numbers] or series: [{name, points: [{label, y}]}]

Every kind takes a title and an optional link, which must be https:. A widget carries up to 20 items or rows, 8 columns, 5 series and 60 points. Everything is text: nothing a routine shows is rendered as markup. Metric and progress values are kept (the last 90) for the sparkline. In an exploration the call only previews the widget; the routine it becomes keeps the real one.

When a request asks to see or track something ("as a widget", "on my dashboard", "show me X every hour"), the explorer and the compiler end the routine with widget.show in the kind that fits; a request to be told something still ends with a message, and a routine may do both. Turn into a widget on a routine's page (POST /api/routines/{id}/widget with an optional kind) re-explores the original request with that change, like a repair, and approving it saves a new version of the same routine.

Judgments and texts#

CallReturnsNotes
judge.<name>(item){p}, the probability of yesFor each name in manifest.judgments. Treat p >= 0.5 as yes. Pass the whole item.
write.<name>(input){text}For each name in manifest.writes. At most 20 per run. It needs a model set up to write.

Both check the daily spending limit before they call a model (judgments are estimated at $0.01 each, texts at $0.02), and both are recorded in Activity.

Data, state and helpers#

GlobalWhat it is
paramsThe resolved parameters, frozen
eventWhat started the run (see Triggers), frozen
state.get(key)A value kept from earlier runs, or null
state.set(key, value)Keeps plain JSON data. All state together must stay under 64 KB.
state.delete(key)Forgets a key
state.keys()The kept keys
routines.run(id, params)Runs a routine listed in uses and returns what its run() returned. Only present when uses is set.
now()The current time as an ISO 8601 string in the owner's zone
log(text)A debug line, saved with the finished run

State is saved only when the run succeeds, so a failed run never leaves half an update. The Memory tab shows it, and Clear memory clears it. A helper routine run with routines.run sees its own settings and state, but cannot change its state. Helpers cannot form a loop and can go at most 3 levels deep.

Dates and money#

All dates are ISO strings in the owner's zone and language.

FunctionReturns
dates.zone()The zone name
dates.today()Midnight today
dates.startOfDay(iso, offsetDays)Midnight of that day plus the offset
dates.addDays(iso, n)iso plus n days
dates.addHours(iso, n)iso plus n hours (fractions allowed)
dates.diffDays(a, b)Whole calendar days from a to b
dates.sameDay(a, b)Boolean
dates.isBefore(a, b)Boolean
dates.weekday(iso)0 for Sunday through 6 for Saturday
dates.format(iso, pattern)Text. Tokens: EEEE (weekday), EEE, d, dd, MMMM (month), MMM, MM, yyyy, HH, mm. Text in single quotes is copied as is: "d 'of' MMMM".
dates.parse(text)ISO date of the first date in free text (2026-09-26, 26/09/2026, 26/09, 26 de setembro, Sep 26, 2026), or null
money.find(text)The first amount as written ("R$ 1.482,35"), or null
money.parse(text)A number, reading Brazilian (1.482,35) or US (1,482.35) style, or null
money.format(value, currency)BRL, USD, EUR or GBP, in the routine's language

A date that is not ISO throws dates: "…" is not an ISO date. Calendar all-day events have a plain date (2026-09-25) as start.

Limits#

LimitValueWhere
The routine's own running time90 s per scheduled run, 5 s in checks and testsinternal/scheduler, internal/routine
Whole run, waits included15 minutesinternal/scheduler
Capability calls per run500 (judgments count, helpers share the total)internal/runtime
Written texts per run20runtime.MaxWrites
Kept state64 KB as JSONruntime.MaxState
Routines running routines3 levelsruntime.MaxDepth
Schedule you setat most every 5 minutesinternal/app
Watch interval5 minutes to 24 hours, default 10 minutesruntime.Watch
Webhook body and rate256 KB, 30 calls a minuteinternal/app/webhooks.go
Approval wait30 minutes; the time a run waits for approval does not count toward its 15 minutesinternal/approval, internal/pause
Repository files1 MB each, no symbolic linksinternal/repo

The time limit counts only the routine's own work. A work clock stops while the routine waits on the host (a capability call, a judgment, a text, a helper routine) and starts again when the answer comes back. A routine that judges 200 emails one by one is therefore bounded by the 15-minute limit, not by the 90 seconds. Past its limit, the run fails with "routine ran past its time limit".

Tests and proofs#

Scenarios#

A test is a named scenario (internal/trace.Scenario). The routine runs against canned data, and Pimpo checks the calls it makes that change something or send something.

FieldMeaning
nameThe test's name
nowThe clock, RFC 3339. Write it in UTC (ending in Z).
responses[{capability, result}], canned results for reads. Calls to the same capability take them in order, and the last one repeats. A capability with no response returns []. The capability may carry its scope (http.getJSON:brapi.dev).
paramsValues for this test. The rest keep their defaults.
eventWhat starts the run: {items: [...]}, {webhook: {...}} or {answer: {...}}
judgments{judgment: {substring of the item: probability}}. An unlabeled item gets 0.05, a clear no.
writes{write: {substring of the input: text}}. Without a match, the text is (name).
stateWhat earlier runs kept
expect_state{key: value} the run must keep. Timestamps compare as instants.
expect[{capability, count?, contains?, not_contains?}]

Every call that is not a read (notify, reversible or irreversible) is recorded, not performed, and answers {ok: true}. An expectation matches the text of every string and number in the arguments, without regard to case. For an expectation on telegram.send, whatsapp.send or notify.send, a call to any of the three counts. A failed check says what was actually sent, so you can tell a wrong routine from a wrong test.

gmail.search and calendar.events are answered like a real server: all the items in their responses form a pool, and each call gets the items that match its own arguments (Gmail query operators, unread, days, max, date ranges). There are two modes:

For a watching routine, a world scenario without event gets one made the way the scheduler would make it: the watched capability, asked with the watch's arguments, returns every item as new.

Traces and the holdout#

A trace (internal/trace.Trace) is a recorded exploration: id, request, now, calls (capability, args, result, error), judgments, questions, outcome, expect, an optional event, and an optional holdout. The holdout is a second world scenario that the compiler never sees. A routine that only memorized the recording fails it.

Example, shortened from testdata/openclaw/oc-23-order-webhook.json:

{
  "id": "oc-23-order-webhook",
  "request": "When my shop sends the new order webhook, tell me on Telegram with the customer's name, the amount and the items.",
  "now": "2026-09-24T14:12:00-03:00",
  "event": {"webhook": {"method": "POST", "query": {}, "body": {
    "order_id": "1042", "customer": {"name": "Marina Souza"}, "total": 189.9, "currency": "BRL",
    "items": [{"name": "Pimpo mug", "qty": 2}, {"name": "T-shirt", "qty": 1}]}}},
  "calls": [
    {"capability": "telegram.send",
     "args": {"text": "🛒 Order 1042 from Marina Souza: R$ 189,90 — 2× Pimpo mug, 1× T-shirt"},
     "result": {"ok": true}}
  ],
  "outcome": "One message for each order the shop sends, with customer, amount and items.",
  "expect": [{"capability": "telegram.send", "count": 1, "contains": ["Marina Souza", "189,90", "Pimpo mug"]}],
  "holdout": {
    "now": "2026-10-08T10:00:00-03:00",
    "responses": [],
    "event": {"webhook": {"method": "POST", "query": {}, "body": {
      "order_id": "1107", "customer": {"name": "Rafael Lima"}, "total": 54.5, "currency": "BRL",
      "items": [{"name": "Stickers", "qty": 5}]}}},
    "expect": [{"capability": "telegram.send", "count": 1,
                "contains": ["Rafael Lima", "54,50", "Stickers"], "not_contains": ["Marina"]}]
  }
}

Explorations in the app have no holdout. The holdout is used by the compiler proof.

Running the proof#

cmd/proof compiles every trace in a folder and checks each routine against the replay, its own tests and the holdout. It compiles with Claude Code by default; -model takes any model the app takes: codex, opencode:<provider>/<model>, ollama:<name>, or <provider>:<name> through its API, with the key in PIMPO_<PROVIDER>_KEY (for example PIMPO_OPENAI_KEY) and the price given with -price-in and -price-out, since a price is never guessed.

make proof                                          # go run ./cmd/proof -workers 4
go run ./cmd/proof -dir testdata/openclaw -out docs/proof/openclaw
go run ./cmd/proof -dir testdata/openclaw -only oc-22,oc-23 -attempts 1
go run ./cmd/proof -out docs/proof/openclaw -reuse -export /tmp/oc-routines
go run ./cmd/proof -model opencode:deepseek/deepseek-v4-pro -out /tmp/proof-deepseek
PIMPO_OPENAI_KEY=… go run ./cmd/proof -model openai:gpt-5 -price-in 1.25 -price-out 10 -out /tmp/proof-openai
FlagDefaultMeaning
-dirtestdata/proofFolder of traces (*.json)
-outdocs/proofWhere results.json and README.md go
-modelsonnetModel for the compiler (see above)
-price-in, -price-outUSD per million tokens, for an API model
-workers3Compilations in parallel
-onlyOnly traces whose id contains one of these, comma-separated
-attempts3Compile attempts, with feedback, as the app makes
-exportAlso write the accepted routines that pass the holdout to this folder, in the repository layout
-reusefalseCompile nothing. Export from the results.json already in -out (needs -export).

The report counts the gate (the first attempt is accepted and passes the holdout), first-attempt acceptance, final acceptance, final holdout passes, and cost. Exported routines take the trace's id without its numbering (oc-23-order-webhook becomes order-webhook). testdata/proof/ and testdata/openclaw/ hold the traces (23 in openclaw, listed in its README).

Writing, importing and sharing by hand#

Repository layout#

routines/
  cotacao-do-dia/
    routine.json   name, description, manifest
    routine.js     the code
    tests.json     a list of tests

Folder names are lowercase letters, digits and dashes, starting with a letter or digit, up to 64 characters. routine.json needs a name, and routine.js must exist. tests.json is optional, but a routine without tests fails the audit, so it cannot be imported.

A complete, valid example, adapted from gallery/routines/dolar-hoje.json (the arrow emoji is left out and only one of its tests is kept):

routine.json

{
  "name": "Exchange rate of the day",
  "description": "The rate in reais of the currency you choose, on weekdays.",
  "manifest": {
    "schedule": "0 10 * * 1-5",
    "capabilities": ["http.getJSON:economia.awesomeapi.com.br", "notify.send"],
    "locale": "pt-BR",
    "params": [
      {"name": "moeda", "label": "Currency", "type": "select", "options": ["USD", "EUR", "GBP"], "default": "USD"},
      {"name": "destinos", "label": "Where to notify", "type": "destinations", "default": []}
    ]
  }
}

routine.js

async function run() {
  const q = (await http.getJSON("https://economia.awesomeapi.com.br/json/last/" + params.moeda + "-BRL"))[params.moeda + "BRL"];
  const change = Number(q.pctChange);
  await notify.send({text: params.moeda + ": " + money.format(Number(q.bid), "BRL") +
    " (" + (change > 0 ? "+" : "") + change.toFixed(2).replace(".", ",") + "% today)"});
}

tests.json

[
  {
    "name": "euro",
    "now": "2026-09-24T07:00:00-03:00",
    "params": {"moeda": "EUR"},
    "responses": [
      {"capability": "http.getJSON", "result": {"EURBRL": {"bid": "6.02", "pctChange": "0.1"}}}
    ],
    "expect": [
      {"capability": "notify.send", "count": 1, "contains": ["EUR:", "6,02", "+0,10%"]}
    ]
  }
]

When you write a routine by hand, follow the rules the compiler follows:

Importing#

pimpo routines import [--active] [--data DIR] FOLDER

FOLDER holds a routines/ directory in the layout above. --data is the data directory and defaults to ~/.pimpo; it is created if it does not exist. Each routine is checked before it is saved:

  1. The manifest is valid.
  2. Every one of its own tests passes.
  3. An audit shows it calls only what it declares.

A routine whose content is already installed is skipped ("already installed"). Routines arrive paused unless you pass --active, so review each routine's settings and tap Resume. A running Pimpo lists them at once. Output lines start with ✓ (installed), = (unchanged) or ✗ (refused, with the reason).

In the app, Routines › Repository does the same with a folder you choose: Send routines to the folder writes every routine in this layout and makes a local commit, Publish (git push) pushes, Fetch changes (git pull) pulls, and each changed routine can be applied (Install or Update) after the same checks. It shows what a change adds (Will now be able to: …) and removes (No longer uses: …).

Exporting#

There is no pimpo routines export command. To get your routines as files, use Send routines to the folder in Routines › Repository. go run ./cmd/proof -export DIR writes compiled proof routines in the same layout.

Publish on a routine's page signs it with your author key, which is made on first use and kept in the vault. A routine without tests cannot be published. You then download the signed entry and open a pull request, as gallery/README.md explains. From the terminal:

pimpo gallery keygen --out KEYFILE
pimpo gallery build --key KEYFILE --author ID --name "Your Name" DIR   # signs DIR/routines/*.json into DIR/index.json
pimpo gallery verify INDEX                                             # file or URL; fails on any problem

Before installing a gallery routine, Pimpo checks all of the following:

Gallery routines install active. When the gallery has a newer version, the routine's settings show New version in the gallery with Update, which keeps your schedule and settings.

Per-routine settings#

Routine settings is at the top of every routine's page. Saving (Save settings) checks every value against the manifest before anything is stored.

SettingWhat it changes
WhenThe schedule: Every day, Weekdays, Days of the week, Every month, Every few hours, Every few minutes (5, 10, 15 or 30) or Advanced (cron). Back to the original schedule restores the manifest's schedule.
Check everyFor a watching routine: 5 minutes to 1 day
The routine's paramsOne field per parameter: a city search for location, a yes/no choice for boolean, destinations with Connect more channels, and so on
Model for judgments and textsShown only when the routine has judgments or writes. It picks the model for this routine alone (Default is the judgment model in Settings › Models) and its thinking level (low, medium, high or max).

A routine started only by its webhook shows "Starts when this routine's webhook is called" instead of a schedule. The webhook switch is the separate Start by webhook card below the settings. A routine with no parameters and no judgments says so and offers Redo with the agent to gain settings.

Routines spend money only on judgments and written texts. Each one is checked against the daily limit before the model is called. When the limit is reached, the call fails, the run fails, and the notice says "The day's spending limit ran out." The routine's page shows what it cost this month, and Routines › Runs shows the cost of each run. Exploring and compiling also need budget left. Each compile attempt is capped at $2. See Configuration for the daily limit and models.

Safety#

What a routine can do:

What it cannot do: reach the network, disk, processes, timers or the clock except through the capabilities and now(), read credentials, or change its own manifest.

Every capability call, from a routine or an exploration, goes through the host (internal/host):

  1. Policy. The protection list is checked first, then the owner's rules (Rules). With no matching rule, the risk decides: reversible changes are allowed and kept undoable, and everything else is allowed. The balanced preset asks before anything irreversible. whatsapp.send_to and ha.critical always ask, whatever the rules say. A guest's changes always wait for the person responsible for them.
  2. Approval. When the verdict is to ask, the run waits at that step. You get Allow, All this run (the rest of this run), Always (this routine and capability from now on) or Deny. A denial or an unanswered request makes the call throw.
  3. Reversible form. When a rule says "make it reversible", gmail.delete becomes gmail.trash and gmail.send becomes outbox.send_later, a send that waits long enough to be cancelled.
  4. Receipt. Every call is written to the event log with its source (routine:<id>#<run>), arguments, result, risk, verdict, rule and approval. Receipts appear in Activity. The only calls left out are successful reads made by watch polls.

Undo (internal/undo) works from the receipt. An archive or trash is restored, a label or draft is removed, a delayed send is cancelled, and a connector can return its own undo step (sheets.append is undone with sheets.clear, for example). Simulated and failed actions cannot be undone, and each action can be undone once.

Before a routine is saved from any source (compiler, repository, import or gallery), its own tests run and the audit runs them again with every capability reachable. Calling anything undeclared, or a host outside the scope, is a refusal. See Threat model for the attackers and defenses, and Extending Pimpo for the judge and guard APIs.

Troubleshooting#

SymptomCause and what Pimpo does
"⚠️ … didn’t run" with Run again and Redo with the agentThe run failed. The routine is now stopped (Needs attention) and does not run on its trigger again until you run, resume or repair it. The error shows on its page.
"routine ran past its time limit"The code itself worked for more than 90 seconds. Waiting on services does not count. Look for a loop over a large result.
"routine made more than 500 capability calls"A loop called a capability, or judged items, too many times. Filter in the query first.
"… is outside the manifest scope"A URL's host is not the declared one. www. counts as part of the host.
"… is not set" or "the routine has no setting …"A parameter has no value, or a setting is left over from an older version. Save Routine settings again.
"state would exceed 64 KB"Keep less: the latest values, the last few ids.
"no model is set up to write text" or "no judgment backend configured"Set a model for judgments in Settings › Models.
"The day's spending limit ran out."A judgment or text would pass the daily limit.
"… needs your approval" or "was not approved"A rule asks, and nobody answered in time or you said no. Always stops the asking for this routine.
"… is already running"The previous run has not finished.
"routine … is stopped after a failure"A helper routine is stopped. Repair it first.
The exploration offers no routineEvery read failed, or it was a one-time reminder.
"the routine did not pass its checks: …"No compile attempt passed. The exploration stays ready, and you can try again.
A run missed while the computer was offIt runs once, late, if it was due less than 12 hours ago. Otherwise it is skipped.

Repair#

Redo with the agent (on the routine's page, in the failure notice, or in Needs you) starts a new exploration of the original request, linked to the routine, telling the agent the error of the last failed run. You watch it and approve it as usual. Approving compiles a new version, and that version must also pass the tests of the version it replaces:

A successful repair saves the new version and makes the routine active again.

This page is built from the repository's documentation. Improve it on GitHub.