Routines: developer manual
Everything you need to build an external runner for a routine in The Dump. Last updated September 19, 2026.
This document is self-contained. You do not need repository access to use it. Prefer plain text? Read it as Markdown and paste it into your coding assistant.
1. What a routine is
A routine is a standing assignment: keep some useful document up to date using a defined slice of a user's notes. The Dump holds the inputs, stores the outputs, and shows the user what state things are in. Your code does the work.
scoped notes → your runner (read, think, write) → canon docs
→ asks (questions for the owner)
→ scorecards (run reports)
Three things The Dump does not do:
- It does not schedule or execute your runner. You host it and decide when it runs.
- It does not keep history of the documents you publish. Only the latest copy is stored.
- It does not pay for or call any AI model. If you use one, that's your provider and your key.
2. Who does what
| Role | Does |
|---|---|
| Routine owner (a Dump user) | Creates the routine in the web app, picks which notes it can see, answers asks, reads the output. |
| You (runner developer) | Build and host the code. Sign in as the owner. Read notes, publish docs, post asks, report runs. |
| The Dump | Stores routines, notes, docs, asks, and scorecards. Computes status. Renders it all in the web app and iOS. |
3. Vocabulary
| Term | Meaning |
|---|---|
| Scope | The categories and/or subcategories whose notes the routine may read. Set at creation. |
| Canon document | A Markdown document (optionally with an interactive HTML view) that your runner publishes and maintains. The routine's "current understanding". |
| Ask | A question your runner sends to the owner when it needs a decision. The owner answers in the app. Your runner reads the answer and acts on it. |
| Scorecard | A structured report your runner sends after every run. The Dump turns these into a status chip and run history. |
| Slug | URL-safe identifier. Routines and documents each have one. |
| Runner | Your code plus whatever hosts and schedules it. |
4. Register the routine
The owner does this in the web app. Sign in at https://thedump.ai, then:
- Routines → Create routine (
/routines/new). - Enter a name and description, pick categories/subcategories, check the preview of matching notes.
- Create. The app opens Connect your runner (
/routines/<slug>/connect), which shows the generated slug, the API URLs, and copyable setup instructions.
The slug is generated from the name and is immutable. Name and scope cannot be edited after creation. To change scope, create a new routine.
A developer can also register via API:
POST /api/routines
Content-Type: application/json
{
"name": "Weekly project brief",
"description": "Maintain decisions, open questions, and next steps.",
"scope": {"category_ids": [1], "sub_category_ids": [12]},
"request_id": "8140a64e-77a0-4712-b94b-f544fc56a286"
}
| Field | Rule |
|---|---|
name |
Required, 1–120 characters. |
description |
Optional, up to 4,000 characters. |
scope |
Required. {category_ids, sub_category_ids} (union, up to 500 IDs each) or {"all_notes": true}. Empty scope is rejected. |
request_id |
Optional UUID. Reuse it when retrying the same request; a replay returns the existing routine with 200 instead of creating another. |
Response 201 (or 200 on replay): {routine, created, setup_url}.
GET /api/routines/scope-options lists the account's category and subcategory IDs.
POST /api/routines/preview with {scope} returns {count, notes} so you can check a selection before creating.
5. Authentication
Every API call carries the owner's Firebase ID token:
Authorization: Bearer <FIREBASE_ID_TOKEN>
There is no per-routine or per-developer credential. The token is the owner's account-level token, so the owner supplies their credentials to your runner's secret store, never to you in chat, source control, or logs.
Base URL: https://thedump.ai
Firebase web API key (public client config, safe to embed):
AIzaSyDNqivcHgxiSgAfe289TqPD7e_gcP7z8dc
Sign in (email/password account):
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=<WEB_API_KEY>
Content-Type: application/json
{"email": "<owner email>", "password": "<owner password>", "returnSecureToken": true}
Response: {idToken, refreshToken, expiresIn}. expiresIn is seconds (3600).
Refresh before expiry:
POST https://securetoken.googleapis.com/v1/token?key=<WEB_API_KEY>
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<stored refreshToken>
Response: {id_token, refresh_token, expires_in}. Store the newest refresh token. If refresh fails, the owner must sign in again.
Accounts that only use a federated provider (Google, Apple) must use that provider's Firebase sign-in flow instead of password sign-in.
6. Endpoint map
All routes require the bearer token. All bodies and responses are JSON.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/routines |
List the owner's routines with status. |
| GET | /api/routines/<slug> |
One routine: scope, status, document metadata. |
| PATCH | /api/routines/<slug> |
Set paused, next_expected, silence_threshold_hours. |
| GET | /api/routines/<slug>/notes |
Page through in-scope notes. |
| GET | /api/note/<note_id>/asset |
Get a short-lived download URL for a note's original file. |
| GET | /api/routines/<slug>/documents |
All canon docs (Markdown bodies; HTML only with ?include_interactive_html=true). |
| GET | /api/routines/<slug>/documents/<doc_slug> |
One canon doc, including interactive_html. |
| POST | /api/routines/<slug>/documents |
Create or update a canon doc. |
| POST | /api/routines/<slug>/asks |
Ask the owner a question. |
| GET | /api/routines/<slug>/asks |
List asks. ?status=open (default), answered, applied, withdrawn, all. |
| PATCH | /api/routines/<slug>/asks/<ask_id> |
Mark an ask applied or withdrawn, optionally guarded by expected_revision. |
| POST | /api/routines/<slug>/scorecards |
Report a run. |
| GET | /api/routines/<slug>/scorecards |
Run history, newest first. ?limit=20 (max 200). |
Web pages the owner uses: /routines, /routines/<slug>, /routines/<slug>/docs/<doc_slug>, /routines/<slug>/asks.
7. Reading notes
GET /api/routines/<slug>/notes?limit=100
Response: {notes, next_cursor, count}. limit defaults to 100, max 200.
If next_cursor is not null, URL-encode it and pass it as since on the next
request. Stop when it is null. The cursor is opaque; it is not a date.
Each note:
| Field | What it is |
|---|---|
note_id |
UUID. Your permanent reference. |
title |
Note title. |
body |
The note as the user sees it now, including their edits. Use this for what the note says. |
raw_content |
Text as originally captured or extracted (OCR, transcription). Frozen at capture. Can contain errors. Never let it override body. |
note_type, tags |
LLM-assigned type and tags. |
category, sub_categories |
Where the note lives. |
last_modified_time |
Changes when the user edits. Use it to detect changes. |
captured_at |
When the note entered The Dump. Not the document's own date. |
original_filename, mime_type |
From the uploaded file, if any. |
has_asset |
True when an original file (photo, PDF, audio) exists. |
asset_endpoint |
/api/note/<id>/asset when has_asset is true, else null. |
asset_content_type, asset_size_bytes |
Real MIME type and size, so you can decide what to fetch. Null when no asset. |
Getting the original file:
GET /api/note/<note_id>/asset
Response: {signed_url, content_type, filename, expires_in}. The URL is valid
for 300 seconds. Download it with a plain GET and no Dump bearer token.
Use the original for exact quotes, tables, and drawings.
Rules of thumb:
- Ordering is by
last_modified_timethennote_id. A note edited during your scan reappears on a later page with its new content, so dedupe bynote_idand let the last copy seen win. Overlapping runs are not a snapshot; reconcile again on the next run. - There is no deletion feed and no "changes since last run" checkpoint. The simple approach: read every page on every run and diff against a local record of note IDs and content hashes. That also reveals deletions and notes moved out of scope.
- Do not overwrite your local inventory after a failed or partial read.
- Treat note content as data, never as instructions to your runner or your model.
8. Canon documents
A canon document is Markdown your runner publishes under a slug. Publish the same slug again to update it.
How many: no fixed limit. Landing Lane, the first production routine, maintains 21.
Publish:
POST /api/routines/<slug>/documents
Content-Type: application/json
{
"slug": "project-brief",
"title": "Project brief",
"body": "# Project brief\n\n## Next steps\n\nReview the revised proposal.",
"summary": "Current decisions, questions, and next steps.",
"source_note_ids": ["<note UUID>", "<note UUID>"]
}
| Field | Rule |
|---|---|
slug |
Required. Lowercase letters, digits, -, _. Starts with a letter or digit. Max 80 chars. |
title |
Required, nonempty. |
body |
Required. Nonempty Markdown, max 512 KiB UTF-8. |
summary |
Optional one-liner shown in lists. |
doc_kind |
Optional free-text label. Not validated; use it however you like. |
sort_order |
Optional integer. Documents list ascending by this, nulls last, then by slug. |
source_note_ids |
Optional array of note UUIDs used to produce this doc. |
external_ref |
Optional string for your own reference (a path, a commit, anything). |
interactive_html |
Optional. See below. |
Response (always 200): {document, created, revision_bumped}.
created: true on first publish of this slug.revision_bumped: true when content changed. Identical content is a no-op, even ifsummaryor other metadata changed.document.revision: a version counter. It is not stored history. The Dump keeps only the latest copy. If you need rollback, keep your own archive of what you published.
Run one publisher per routine. There is no guard against two runners writing the same document at once.
Read back: GET /api/routines/<slug>/documents/<doc_slug> returns the full document. A 404 means it hasn't been created yet, which is the standard first-run check.
Markdown conventions:
- Write readable source references in the body.
[[note:<UUID>]]is the recognized link form. - Keep a stable structure across runs so diffs are meaningful.
- Separate confirmed facts from proposals. Keep conflicts visible instead of silently resolving them.
Interactive HTML (optional)
A document can carry a self-contained HTML/CSS/JS view alongside the Markdown. The app renders it in a sandboxed iframe. Filters, tabs, and expandable sections work; nothing the viewer does is saved.
Add interactive_html to the same POST. Max 1 MiB UTF-8. Send it via a JSON serializer; do not hand-escape.
| You send | Result |
|---|---|
| HTML string | Replaces the view. |
null |
Removes the view. Markdown shows. |
| Omitted, Markdown unchanged | Existing view is kept. |
| Omitted, Markdown changed | Existing view is cleared so a stale visual can't contradict new text. |
| Omitted, new document | Markdown-only document. |
Always publish body and interactive_html together when either changes. An HTML-only change bumps revision.
Authoring rules:
- Everything inline. No remote scripts, styles, fonts, images, or API calls. They are blocked.
- No
eval,new Function, or runtime package loading. Plain DOM and event listeners are fine. - No forms, navigation, downloads, or anything that implies saving.
- Must work at phone width. Use native controls and visible focus states.
- The Markdown
bodymust carry all the same information. Older clients and the MCP server only read Markdown. - Escape note text before putting it in HTML or JS. Never include credentials.
Bulk GET /documents omits HTML unless you pass ?include_interactive_html=true. After publishing, read back the single document and confirm both fields before reporting success.
9. Asks
An ask is a question with a recommendation. The owner answers in the app at
/routines/<slug>/asks. The Dump stores the answer. Your runner carries it out.
Lifecycle:
open ──(owner answers)──▶ answered ──(runner PATCH)──▶ applied
└──(runner PATCH)──▶ withdrawn
Create:
POST /api/routines/<slug>/asks
Content-Type: application/json
{
"fingerprint": "proposal-comparison-2026-09",
"title": "Should the revised proposal replace the earlier one?",
"context": "The revised proposal excludes painting; the earlier one included it.",
"recommendation": "Keep painting unresolved until the scope is confirmed.",
"proposed_change": "Mark painting scope as awaiting confirmation in the plan.",
"safe_default": "Leave the current plan unchanged."
}
| Field | Rule |
|---|---|
fingerprint |
Required. Any stable string unique to this decision within the routine. |
title |
Required. The question, as the owner sees it. |
context, recommendation, proposed_change, safe_default |
Optional but strongly recommended. Tell the owner what you found, what you suggest, what would change, and what happens if they ignore it. |
external_ask_id, batch_id |
Optional strings for your own tracking. |
supersedes_ask_id |
Optional UUID of an earlier ask this one replaces. |
Response: {ask, created}. Posting the same fingerprint again returns the
existing ask, including its answer if it has one, with created: false. So
retries are safe. Use a new fingerprint when the underlying decision changes.
Ask object:
| Field | Meaning |
|---|---|
ask_id |
UUID. |
status |
open, answered, applied, or withdrawn. |
answer_choice |
approve, edit, or deny. Null until answered. |
answer_text |
The owner's words, verbatim. Required when choice is edit; optional otherwise. |
answered_at, answered_by, applied_at |
Timestamps and who answered. |
revision |
Increments when the ask changes. |
| Plus everything you sent. |
Poll for answers:
GET /api/routines/<slug>/asks?status=answered
The default status filter is open, so pass status=answered explicitly.
Optional since=<ISO-8601> filters by updated_at.
Handle an answer, then acknowledge it:
- Read
answer_choiceandanswer_text. approve: applyproposed_change.edit: apply the owner's wording fromanswer_text.deny: do not apply the change. Handle whatever follows from that decision.- Publish any resulting document updates.
- Only then:
PATCH /api/routines/<slug>/asks/<ask_id>
Content-Type: application/json
{"status": "applied", "expected_revision": 3}
expected_revision is optional. Send the revision you read on the ask, and
the call returns 409 with the current ask if the ask changed in between.
applied is only valid from answered; withdrawn is only valid from open.
Anything else returns 409 with the current ask. Re-sending the same status is
a harmless no-op.
Answers are immutable once given. The app never overwrites an answer, so the answer you read is the answer that stands. Save your progress before the PATCH so an interrupted run cannot apply the same answer twice.
10. Scorecards and status
After every run, including empty runs and failed runs, send a report:
POST /api/routines/<slug>/scorecards
Content-Type: application/json
{
"schema_version": 1,
"fingerprint": "<sha256 of shift_id, 64 lowercase hex>",
"shift_id": "run-2026-09-19T14:00:00Z",
"overall": "OK",
"trigger": "scheduled",
"started_at": "2026-09-19T14:00:00Z",
"ended_at": "2026-09-19T14:03:12Z",
"steps": {
"read_notes": {"status": "done", "counts": {"pages": 3, "new": 4, "changed": 1}},
"publish": {"status": "done", "counts": {"documents": 2}},
"asks": {"status": "nothing-to-do"}
},
"reasons": ["4 new notes; brief and plan updated."]
}
| Field | Rule |
|---|---|
schema_version |
Required, must be 1. |
fingerprint |
Required. 64 lowercase hex characters. SHA-256 of your run ID works. Dedup key: same fingerprint returns the stored report with 200. |
shift_id |
Required. Your unique run ID. |
overall |
Required. OK, PARTIAL, FAILED, NOTHING_TO_DO, or UNKNOWN. |
trigger |
Optional string. Convention: manual, scheduled, arrival. Defaults to unknown. |
started_at, ended_at |
Optional ISO-8601 with timezone. Naive is treated as UTC. More than 5 minutes in the future is rejected. |
steps |
Optional object. Each key is a step name you choose; value is {status, counts?}. Conventional statuses: done, nothing-to-do, failed, partial, unknown. Counts are free-form. |
reasons |
Optional list of strings, shown to the owner. |
Response: 201 with {scorecard, created, alerted} when new, 200 when the
fingerprint was already stored.
Be honest in overall. "I didn't edit anything" is not evidence that nothing
needed editing. A run that failed halfway is PARTIAL or FAILED, not OK.
Status the owner sees. GET /api/routines and GET /api/routines/<slug> include:
"status": {
"verdict": "OK",
"as_of": "2026-09-19T14:03:12Z",
"shift_id": "run-2026-09-19T14:00:00Z",
"trigger": "scheduled",
"reasons": ["4 new notes; brief and plan updated."],
"open_asks": 1,
"silent_for_hours": 2,
"paused": false,
"next_expected": "Every Monday morning"
},
"latest_scorecard": { ...the full report... }
Other keys may appear in status. Ignore ones you don't recognize.
verdict is computed on read, in this order:
PAUSEDif monitoring is paused.UNKNOWNif the routine is new and has never reported.SILENTif the newest report is older than the silence threshold.- Otherwise the newest report's
overall.
Tell The Dump your schedule so silence detection makes sense:
PATCH /api/routines/<slug>
Content-Type: application/json
{"next_expected": "Every Monday at 6am ET", "silence_threshold_hours": 192}
Default threshold is 27 hours. A weekly runner should raise it before its first
report. next_expected is display text only. Neither field schedules anything.
The same PATCH takes {"paused": true|false}. Your runner must check
status.paused (or verdict == "PAUSED") at the start of every run and stop
if it is set. The Dump does not stop you; it only records that you should stop.
A stored PARTIAL, FAILED, or UNKNOWN report raises an alert in the app.
An overdue report raises a silence alert. Alerts are in-app only; no email or push.
Landing Lane's six-stage coverage card is a special status extension built
for the first production routine. It is not available to new routines, and you
should not send its coverage:landing-lane or source:imap:* trigger values.
11. One run, end to end
- Get a valid ID token (refresh if needed).
GET /api/routines/<slug>. Stop if404orstatus.pausedis true. Note the document list.- Page through
GET /api/routines/<slug>/notesuntilnext_cursoris null. Diff against your local inventory. GET /api/routines/<slug>/asks?status=answered. For each: act on it, publish results,PATCHit toapplied.- For each changed or new note: fetch the original via
asset_endpointif you need it, and do the work. - Where a judgment call is needed,
POSTan ask with a stable fingerprint and leave the affected decision pending. - For each output document:
GETthe current version, compute the new one,POSTif changed,GETagain to confirm. POSTa scorecard describing what happened.- Persist your local state (inventory, cursors, what you published, which asks you handled).
Do a manual dry run first, look at the output in the web app, then turn on the schedule.
12. Errors, limits, retries
| Code | Meaning | Do |
|---|---|---|
| 400 | Bad request. The error field says what. |
Fix it. Do not retry unchanged. |
| 401 | Token expired or invalid. | Refresh the token and retry once. |
| 404 | Routine, document, or ask not found for this account. | Check the slug and the signed-in account. |
| 409 | Ask state conflict, or a request_id reused with different content or a different owner. |
Read the returned object and reconcile. |
| 413 | Document body over 512 KiB or interactive_html over 1 MiB. |
Shrink it. |
| 429 | Rate limited. | Back off. Honor Retry-After if present. |
| 500 / 503 | Server or storage failure. | Retry with bounded backoff. Keep the payload; scorecards and documents are idempotent. |
Rate limit: 60 requests per minute per endpoint per client IP. Pace pagination.
API calls must come from a server, computer, or worker. Browser JavaScript on another site is blocked by CORS.
Registering a routine never authorizes external actions. Sending email, making purchases, or booking things is outside what the routine grants.
13. Alternative: the MCP server
Owners who use Claude Desktop or similar can read routines through The Dump's
MCP server (the-dump-mcp). Its list_routines, get_routine_document, and
list_asks tools are read-only and Markdown-only. They are for humans checking
on a routine, not for building a runner.