API reference

Every platform primitive Annulo gives a project: local functions and ctx, tables, schedules, tasks, plugins, page endpoints and the CLI. Start here to write a template or plugin, or to see what the assistant can do.

Capability version 33Source
Basics

Overview

Annulo only provides primitives: general capabilities that let the assistant (or you) build business features in a project and keep them running. The business itself (how a platform posts, what a table stores) lives in the project; Annulo knows no specific platform.

A project is a folder, and primitives are used through these files and endpoints:

PathPrimitives
local/*.tsLocal functions and ctx: tables, network, browser, shell, models, MCP, keys
tables/<table>.jsonTables
schedules/<id>.jsonSchedules
tasks/, prompts/, skills/, ANNULO.mdTasks and notes
annulo.json, user/Project manifest
plugins/<id>/Plugins
pages/Page endpoints
Capability version

The current capability version is 33. Each new primitive bumps it (history). If a template uses a primitive from a given version, set "min_annulo_api": <version> in annulo.json: older Annulo builds then ask the user to update before upgrading, and refuse the merge.

Old names

After the Shuttle → Annulo rename, the old names still work: shuttle.json / SHUTTLE.md / /_shuttle/ / X-Shuttle / the shuttle command / SHUTTLE_* env vars.

Online and offline

Offline projects (default) keep data in local SQLite; online projects (creght connected) keep it on creght. ctx.db has the same names and parameters in both. Items marked [online] only work in online projects.

Local functions

Local functions

Deterministic, repeatable logic goes into local functions. Page buttons, schedules and the assistant call them directly, without the model.

Writing one
  • Put files in local/<file>.ts; names match ^[a-z0-9][a-z0-9_-]*$. Files starting with _ (like local/_api.ts) hold shared code, imported by relative path;
  • Export export function name(input, ctx), optionally async. It's called as file.function; in a plugin, <plugin>/<file>.<function>;
  • throw new Error(msg): the message is shown to the user as is; the stack goes to the log;
  • Return something JSON-serializable.
// local/leads.ts
export async function stats(input: { days?: number }, ctx) {
  const since = new Date(Date.now() - (input.days ?? 30) * 864e5).toISOString()
  const { list } = ctx.db.aggregate('leads', {
    filter: [{ field: 'created_at', op: 'gte', value: since }],
    group_by: [{ field: 'created_at', trunc: 'day', as: 'day' }],
    metrics: [{ op: 'count' }],
  })
  return list
}
Runtime
  • Bundled by esbuild to CommonJS (ES2017) and run in an embedded JS engine. No npm `require`; talizen is type-only;
  • Globals: fetch, console, URL (common fields and searchParams.get/has), btoa / atob, TextEncoder / TextDecoder;
  • No setTimeout; use await ctx.sleep(ms);
  • A run lasts at most 60 minutes.
Calling
annulo run                                  # 列出所有函数 / list functions
annulo run leads.stats --input '{"days":7}'
annulo run leads.stats --input @input.json

Progress goes to stderr, the result to stdout. From pages, see Page endpoints. Logs: annulo logs --fn leads.stats.

Local functions

ctx at a glance

The function's second argument. The "Cloud" column says whether it works in cloud functions.

MemberSignatureWhat it doesCloud
ctx.dbsee Reading and writing tablesquery / aggregate / get / insert / update / delete✓
fetchawait fetch(url, { method, headers, body })Global fetch; no localhost or private networks. See Network✓
ctx.fetchAllawait ctx.fetchAll([url | { url, method, headers, body }])Up to 4 in parallel, results in input order✗
ctx.htmlctx.html(text) → .find(css) / .text() / .markdown()Parse HTML✗
ctx.browsersee BrowserDrive local Chrome with your signed-in sessions✗
ctx.execawait ctx.exec(cmd, args, { cwd?, input?, timeout?, env? })Run command-line tools. See Shell✗
ctx.secrets.get(name) → string | nullRead Settings → Keys, or an env var✗
ctx.oauthawait ctx.oauth('google', { account? }) → access_tokenAccounts authorized in Settings → Connections; ctx.oauth.accounts('google') lists them✗
ctx.mcpawait ctx.mcp(server, tool, args?)Call a connected MCP tool; JSON results parsed. ctx.mcp.servers() shows statuscreght only
ctx.llmawait ctx.llm(prompt) or ctx.llm({ system, prompt }) → stringOne answer from the current model; no tools, not in any chat✗
ctx.llm.providers() → [{ id, name, kind, models, … }]Providers in Settings → Models, without keys✗
ctx.llm.fetchawait ctx.llm.fetch(providerId, path, init) → ResponseCall a provider's API; Annulo fills in the base URL and auth (streamable)✗
ctx.agent.current() → { id, name, provider, model, ready, error }The assistant's current model and whether it can run✗
ctx.progress({ done, total, message })Report progress to the page and CLIno-op
ctx.log(...args)Write to the run logno-op
ctx.sleepawait ctx.sleep(ms)Up to 60 sno-op
ctx.locale'zh' | 'en'UI language✓
ctx.workspaceobjectproject_id, site_id, offline, machine: { id, name }, logs_dir, annulo_api…undefined
ctx.chat_idstringThe chat id when the assistant runs it in a chat; '' for buttons and schedules''

Use ctx.llm only for short, unattended judgments (like scoring each new inquiry). Long writing a user asks for with a button belongs in a task.

Local functions

Reading and writing tables

ctx.db reads and writes only declared tables. Calls are synchronous.

MethodReturns
query(table, { where?, filter?, order_by?, limit?, offset?, cursor? }){ total, list, limit, next_cursor }; limit defaults to 20, 1–1000
aggregate(table, { where?, filter?, group_by?, metrics, order_by?, limit?, timezone? }){ list, truncated }; up to 10,000 groups
get(table, id)a row or null
insert(table, data)the new row (with id)
update(table, id, data){ ok: true }; shallow merge at top level, null removes a field
delete(table, id)—
const { total, list, next_cursor } = ctx.db.query('articles', {
  where: { status: 'draft' },
  filter: [{ field: 'words', op: 'gte', value: 800 }],
  order_by: 'updated_at desc',
  limit: 50,
})
const row = ctx.db.insert('articles', { title: 'Hello', status: 'draft' })
ctx.db.update('articles', row.id, { status: 'published', draft_note: null })
Conditions
  • where: field equality, { channel_id: 'x' }. A scalar also matches an item in an array field; where.id reads by record key;
  • filter: [{ field, op, value }], or { match: 'and', conditions: [{ fieldId, operator, value }] }; conditions are ANDed;
  • Operators: eq (default), neq, in, gt, gte, lt, lte, between (value [from, to], inclusive). Numbers compare numerically, strings lexically; mismatched types never match. Mistakes throw instead of silently not filtering;
  • order_by: 'date desc', comma-separated; newest first by default;
  • cursor: for reading a whole table. Pass '' first, then the previous next_cursor; empty means done. Can't be combined with order_by or offset.
Aggregation
  • group_by: ['field', { field, trunc: 'day' | 'week' | 'month' | 'year', as }]; weeks start on Monday;
  • metrics: [{ op: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'first' | 'last', field, as, order_by }]; first / last follow the metric's order_by within the group;
  • Output columns default to <op>_<field>, and count for count; avg keeps 6 decimals;
  • timezone defaults to Asia/Shanghai.

System fields: id, created_at, updated_at. The assistant's db_query / db_aggregate tools take the same parameters.

Local functions

Network

fetch

Global fetch(url, { method, headers, body }). The Response has:

  • ok, status, statusText, url, headers.get(name), headers.forEach(fn);
  • text(), json(), arrayBuffer();
  • body.getReader() for streaming; read() returns { done, value: Uint8Array };
  • timing: { ttfb_ms, total_ms }, truncated.

Limits: no localhost or private networks; bodies up to 10 MB (beyond that text() truncates and truncated is true); up to 2 minutes for headers, then the connection drops only after a full minute without data. No total time limit.

ctx.fetchAll

Send several requests at once, up to 4 in parallel, results in input order. A failed item is { ok: false, status: 0, url, error } and doesn't fail the batch.

ctx.html

ctx.html(text) parses HTML: .find(css) returns [{ tag, text, attrs }], .text() gives plain text, .markdown() converts to Markdown. Invalid selectors throw.

Local functions

Browser

ctx.browser drives Chrome on this computer. Each profile is its own signed-in session, stored locally. Social publishing and collecting rely on it: same device, same network, just like doing it yourself.

const b = await ctx.browser.open({ profile: 'x-main', url: 'https://x.com/home' })
await b.waitFor('[data-testid="tweetTextarea_0"]')
await b.type('[data-testid="tweetTextarea_0"]', input.text)
await b.click({ text: 'Post' })
await b.close()
  • ctx.browser.open({ profile, url?, show?, offscreen?, keep_open? }) → b; keep_open needs show: true;
  • ctx.browser.profile(name) → { id } | null: whether the profile exists.
MethodWhat it does
goto(url, { wait?, timeout? })Open a URL
waitFor(sel, { visible?, timeout? })Wait for an element
exists(sel)Whether an element exists
click(sel | { text }, { timeout? })Click
type(sel, text, { clear?, timeout? })Type
press(key)Press a key
upload(sel, files)Upload: URLs (streamed, up to 2 GB), /_annulo/uploaded/…, 'local:<name>', or a screenshot's { file }
eval(fn | string)Run in the page; return JSON-serializable values
text(sel?) / html(sel?) / url()Read text, HTML, current URL
listen(pattern) / responses(pattern, { min?, timeout? })Record and fetch matching API responses [{ url, status, json, text }]
screenshot({ selector?, fullPage? }) → { file }Screenshot
snapshot({ label }) → { dir }Save the current state on purpose
setContent(html)Put HTML into the page
close()Close

Each action waits up to 30 s by default. When goto / waitFor / click / type / upload / responses fail, Annulo saves a snapshot (screenshot, actionable elements, HTML) and the error carries err.snapshot, which the assistant uses to fix the script.

Local functions

Shell, keys, MCP, models

ctx.exec

await ctx.exec(cmd, [args…], { cwd?, input?, timeout?, env? }) → { code, stdout, stderr, truncated }

  • No shell: command and arguments are separate. For pipes or redirects use ctx.exec('sh', ['-c', …]);
  • Commands resolve on the login shell's PATH; cwd is relative to the project root and can't leave it;
  • timeout defaults to 30 s, max 10 minutes; stdout and stderr keep 10 MB each;
  • A non-zero exit code doesn't reject; check code. It rejects only for a missing command, a timeout or a bad cwd.
ctx.secrets

ctx.secrets.get(name) reads a value from Settings → Keys (falling back to an env var of the same name), or null. Keys stay on this computer and the assistant can't read them.

ctx.oauth

await ctx.oauth('google', { account? }) returns an access token for an account authorized in Settings → Connections (Search Console, GA4, read-only). With several accounts, pass account; ctx.oauth.accounts('google') lists them, oldest first.

ctx.mcp

await ctx.mcp(server, tool, args?) calls a tool from Settings → MCP; JSON text results are parsed, and tool errors reject. ctx.mcp.servers() returns [{ name, status }] with status connected / needs_auth / connecting / failed / disabled.

ctx.llm
  • await ctx.llm('prompt') or ctx.llm({ system, prompt }): one answer from the current model, as a string;
  • ctx.llm.providers(): providers [{ id, name, kind: 'api' | 'cli', api, base_url, enabled, ready, models: [{ id, model, name, web_search }] }], without keys;
  • ctx.llm.fetch(providerId, path, init): call a provider's API directly; Annulo fills in the base URL and auth, and the body can be streamed. Only for kind === 'api' providers.
Cloud and phone

Cloud and remote functions

[online] By default, local functions can only be called inside Annulo. Once a project is connected to creght, its back office also opens on a phone or another computer. Where do the functions run then? You declare it in the function file:

  • export const cloud = [...]: runs on creght, even with your computer off. Tables only;
  • export const remote = [...]: runs on your computer, which must be on. Everything local works (browser, keys, shell).
Example: checking Xiaohongshu stats on the go

You're on the subway and open the back office's Social page on your phone:

  1. The page shows likes over the last 30 days. That's just reading and summing a table, which creght can do on its own, so it doesn't matter that your computer at home is off → cloud;
  2. The numbers look stale, so you tap "Collect now". Collecting means opening the Xiaohongshu profile with your signed-in account, and only your computer has that → remote: creght relays the call to Annulo running at home, which opens the browser, reads the numbers and writes them to the table, streaming progress back to your phone;
  3. When nobody taps anything, a schedule collects every 6 hours on the computer.

One file, two functions, each declaring where it runs:

// local/xhs.ts
export const remote = ['collect']   // needs the browser's signed-in session: only on your computer
export const cloud = ['summary']    // reads tables only: runs on creght, even with the computer off

/** Open the profile, read each note's likes, record today's numbers */
export async function collect(input: { channel_id: string }, ctx) {
  const ch = ctx.db.get('social_accounts', input.channel_id)
  ctx.progress({ message: `Opening "${ch.name}"…` })   // shown on the phone's button too

  const b = await ctx.browser.open({ profile: ch.profile, url: ch.home_url })
  const notes = await b.eval(() =>
    [...document.querySelectorAll('section.note-item')].map((el) => ({   // selectors follow the platform's page
      id: el.getAttribute('data-id'),
      likes: Number(el.querySelector('.count')?.textContent ?? 0),
    })),
  )
  await b.close()

  const today = new Date().toISOString().slice(0, 10)
  for (const n of notes) ctx.db.insert('social_post_daily', { channel_id: ch.id, post_id: n.id, date: today, likes: n.likes })
  return { notes: notes.length }
}

/** Total likes per day over the last N days */
export function summary(input: { channel_id: string; days?: number }, ctx) {
  const since = new Date(Date.now() - (input.days ?? 30) * 864e5).toISOString().slice(0, 10)
  return ctx.db.aggregate('social_post_daily', {
    where: { channel_id: input.channel_id },
    filter: [{ field: 'date', op: 'gte', value: since }],
    group_by: ['date'],
    metrics: [{ op: 'sum', field: 'likes', as: 'likes' }],
  }).list
}
// schedules/xhs-collect.json: when nobody taps, the computer collects every 6 hours
{ "name": "Collect Xiaohongshu", "fn": "xhs.collect", "every": "6h", "input": { "channel_id": "<account id>" } }

Then run annulo push once: summary is bundled into a site function on creght; collect needs no bundling, since Annulo reports it when it connects.

Where it runs
Called byxhs.summary (cloud)xhs.collect (remote)
A button inside Annulothis computerthis computer
A button on the phonecreght, even with the computer offrelayed by creght → your computer (Annulo open)
A schedule—this computer
The assistant via annulo runthis computerthis computer

Pages don't have to care: inside Annulo they call local/run; outside it, cloud functions are called as the site function local/<file>, and remote ones go through the site function shuttle.call to your computer. The template's runLocal('xhs.summary', …) wraps this, so the same line works in both places.

Rules for cloud
  • Only project members can call them;
  • Available: ctx.db, ctx.locale, ctx.mcp('creght', …) (read-only, owner only), ctx.mcp.servers(); progress / log / sleep are no-ops and workspace is undefined;
  • Using secrets, fetchAll, html, oauth, browser, llm, exec or agent throws. That's why collect above can't be cloud.
Rules for remote
  • Annulo must be open on the computer, with "Allow remote use of this computer" on in Settings → Remote access (off by default);
  • Only the project owner can call them, and the computer only runs functions listed in remote;
  • The function runs on the computer as usual, and ctx.progress updates stream back to the phone.

A function can't be in both cloud and remote.

Project files

Tables

One file per table, tables/<table>.json; the file name is the table name (^[a-z][a-z0-9_]{1,40}$).

{
  "name": "询盘",
  "desc": "网站表单和邮件进来的询盘",
  "json_schema": {
    "type": "object",
    "properties": {
      "email":   { "type": "string", "description": "客户邮箱" },
      "score":   { "type": "integer", "description": "意向分 0-100" },
      "tags":    { "type": "array" }
    },
    "required": ["email"]
  }
}
  • Declare it and it's readable and writable: offline projects need no setup; online projects create missing tables when the files change;
  • Only declared tables can be used, by ctx.db, the page's db/<table>, and the assistant's tools alike;
  • Plugin tables are prefixed: plugins/<id>/tables/t.json becomes <id>_t;
  • System fields: id, created_at, updated_at;
  • json_schema documents the table for people and the assistant; Annulo doesn't validate writes against it.
Project files

Schedules

One file per schedule, schedules/<id>.json; the file name is the id.

// schedules/collect.json
{ "name": "采集社媒数据", "fn": "social.collect", "every": "6h" }

// schedules/weekly.json
{ "name": "写周报", "task": "weekly-report", "every": "7d", "at": "09:00" }
FieldMeaning
fn / task / promptPick one: run a local function; run tasks/<id>.md; hand a prompt to the assistant in a new chat
every30m / 6h / 1d / 7d; at least 5 minutes
atHH:MM, local time; wins over every when both are set
inputArguments for the function or task
nameDisplay name
  • Runs only while Annulo is open; a missed run happens once the next time it opens;
  • A task / prompt run lasts up to 30 minutes and only runs in the open project; other projects run fn schedules only;
  • Plugin schedule ids are <plugin>/<id>, with fn / task written in full.
Project files

Tasks, prompts and notes

Tasks tasks/<id>.md

Work for the assistant (articles, ideas, weekly reports). Frontmatter takes three single-line fields: name, description, thinking (off / minimal / low / medium / high / xhigh / max); the body is the instruction. Page buttons run it in a background chat via local/tasks/<id>/run. Plugin task ids are <plugin>/<task>.

Prompts prompts/<id>.md

The part users care about ("how to write": structure, tone, length) lives in its own file: the template's default prompts/<id>.md, and the user's edits from the page in user/prompts/<id>.md, which wins when present. Rules for fetching data, saving and formats stay in the task file.

Skills skills/<name>/SKILL.md

Instructions for the assistant. Frontmatter has name (lowercase letters, digits, -, matching the folder) and description.

Project notes
  • ANNULO.md: the project overview, added to the system prompt of every new chat, up to 8,000 characters;
  • INSTRUCTIONS.md: the user's "Instructions for the assistant" from Settings → Projects; wins over the project notes on conflict.
Project files

Manifest and user/

annulo.json
{
  "name": { "zh": "自媒体工作台", "en": "Creator studio" },
  "description": { "zh": "…", "en": "…" },
  "min_annulo_api": 33,
  "plugins": { "social": "https://github.com/annulo/plugins#social" },
  "assistant": {
    "intro": { "zh": "…", "en": "…" },
    "suggestions": { "zh": ["…"], "en": ["…"] }
  }
}
FieldMeaning
name / descriptionTemplate name and description, a string or { zh, en }
min_annulo_apiRequired capability version; older Annulo builds refuse to merge the upgrade
plugins{ "<id>": "<repo>#<dir>" }, installed when a project is created
assistantThe assistant panel: name, intro, suggestions (each a string or { zh, en })

To support Annulo builds below capability version 26, also ship a shuttle.json.

user/

The user's own customizations: user/prompts/, user/annulo.json (plugins the user installed or removed; null drops one the template brings), user/plugins/<id>/…. Templates and plugins never write here, so upgrades never conflict with them. To leave room for customization: ship a default, store the user's version under user/, and prefer it when present.

Template versions

A template is a directory with annulo.json in a git repo. Versions are semver tags (v1.2.0; pre-releases aren't offered), and the tag message is the upgrade note. Upgrades are three-way merges in the project's git.

Project files

Plugins

A plugin is a bundle of features any project can install, in plugins/<id>/ (id matches ^[a-z][a-z0-9]{1,19}$), upgraded separately from the template.

FileMeaning
plugin.json{ name, description, min_annulo_api }; name / description can be { zh, en }
PLUGIN.mdNotes for the assistant
local/, tables/, tasks/, prompts/, schedules/, skills/, components/Same as the project's folders
  • No pages; the template or the assistant builds those;
  • Functions are <id>/<file>.<fn>, tables <id>_<table>, task and schedule ids <id>/<name>;
  • User edits to prompts go to user/plugins/<id>/prompts/<task>.md;
  • Publishing: a subdirectory with plugin.json in a repo, versioned by semver tags. Example: annulo/plugins.
Pages and CLI

Page endpoints

Each file under pages/ is a route (react-router 7, client-side only), rendered by Annulo on your computer and refreshed on save. Pages use primitives through same-origin endpoints under /_annulo/api/, and must send the header `X-Annulo: 1`. Responses carry an X-Annulo header, which pages use to tell whether they're inside Annulo.

const res = await fetch('/_annulo/api/local/run', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Annulo': '1' },
  body: JSON.stringify({ fn: 'leads.stats', input: { days: 7 } }),
})
// SSE:data: {"type":"progress",...} … data: {"type":"result","data":{"value":…,"ms":…}}
EndpointWhat it does
GET local/functions{ list: [{ name, file }] }
POST local/run{ fn, input }; returns SSE: start, progress, log, then result { value, ms } or error { message }. Keeps running if the page closes
POST local/runs/<id>/abortStop a run (id in the x-annulo-run header)
GET local/logs?fn=&id=&limit=Run logs
GET local/tasks, GET local/tasks/<id>Tasks: notes, prompt, running chats, last result
PUT local/tasks/<id>{ prompt } edits the prompt; { reset_prompt: true } restores the default
POST local/tasks/<id>/run{ input? } → { chat_id }; runs the task in a background chat; 409 if the same run is in progress
POST local/ask{ text, title? } → { chat_id }; hands a request to the assistant in a background chat
GET agent/runningChats in progress; poll it to see when handed-off work is done
GET local/schedules, POST local/schedules/<id>/run, …/toggleSchedules: list, run now, pause / resume
GET db/<table>{ list }: the whole table, newest first
POST db/<table>, PATCH db/<table>?id=, DELETE db/<table>?id=Create, merge-update, delete a row
POST local/uploadmultipart field file, up to 200 MB → { url, … }
POST local/filesBody is the file, header X-Filename, up to 8 GB → { ref: 'local:<name>', url } (kept locally, for browser uploads)
PUT local/secrets, GET local/secrets?names=A,BWrite a key; check which are set (values never returned)
POST fetchProxy a request { url, method, headers, body }; responses up to 5 MB; no private networks
GET /_annulo/img?url=Image proxy (no Referer), up to 20 MB per image
Talking to the Annulo shell

Pages send window.parent.postMessage(msg, location.origin):

  • { type: 'annulo:open-chat', chat_id }: open a chat on the right;
  • { type: 'annulo:navigate', view: 'settings' }: open Annulo's settings;
  • { type: 'annulo:reload-backend' }: reload the back-office page.

The shell sends shuttle:refresh (the assistant changed data; reload quietly) and shuttle:theme { theme }.

[online] Outside Annulo (on a phone), pages call site functions instead: cloud functions run on creght, remote ones on your computer. See Cloud and remote functions.

Pages and CLI

The annulo CLI

Commands talk to the Annulo running on this computer; the assistant uses them in its shell too.

CommandWhat it does
annulo run [<file.fn>] [--input JSON|@file]Run a local function; with no argument, list them
annulo logs [--fn name] [--id runId] [--limit N]Run logs (up to 100)
annulo upload <files…> [--json]Upload local files into the project and print their URLs
annulo push [-m msg]Commit the project's git; [online] then merge remote changes, push to the creght preview and build cloud functions
annulo templateThe project's template version and release notes
annulo template upgrade [--to N]Three-way merge to the latest (or a given) version
annulo mcp listConnected MCP servers and status
annulo mcp add <name> <url>Add a remote MCP; -H 'Authorization: Bearer ${KEY}' adds a header (${key name} is substituted)
annulo mcp add <name> -- <cmd> [args…]Add a local MCP; -e K=V for env vars, --force to replace
annulo mcp remove <name>Remove an MCP server

The old shuttle command still works.

Pages and CLI

Assistant tools

Annulo adds very few tools to the assistant: only things it does constantly where arguments are easy to get wrong. Everything else goes through the CLI (annulo run and friends). With Claude Code / Codex as the assistant, the same tools come through Annulo's MCP.

ToolParametersWhat it does
db_query{ table, where?, filter?, order_by?, limit?, fields?, cursor? }Read-only table query, limit up to 200
db_aggregate{ table, where?, filter?, group_by?, metrics, order_by?, limit?, timezone? }Grouped aggregation, same as ctx.db.aggregate
page_errors{ reload?, path?, wait_seconds? }Check the back-office page for errors; used to verify page edits
request_user_input{ title?, description?, questions: [{ id, type, label, options?, … }] }Ask the user 1–6 questions (single / multi select, short / long text); the turn ends and the answers arrive as the next message

Connected MCP tools are named mcp__<server>__<tool>.

Pages and CLI

Capability versions

VersionAdded
33Offline upload URLs without a port, /_annulo/uploaded/…; annulo upload
32ctx.chat_id
31thinking in task frontmatter
30ctx.mcp.servers(), ctx.agent.current(), provider kind
29ctx.exec
28Plugins
27assistant in annulo.json
26Annulo names (old names still accepted)
25web_search flag on models
24Streaming fetch bodies, TextEncoder / TextDecoder, arrayBuffer; ctx.llm.providers / ctx.llm.fetch; ctx.fetch removed
23Multi-account ctx.oauth, ctx.oauth.accounts
22The assistant on a phone; offline projects, workspace.offline
21export const remote
20export const cloud
19ctx.db filter syntax, offset, total
18Pages rendered locally, react-router
17ctx.browser.profile
16One file per table and schedule
15workspace.machine
14where.id; run logs, annulo logs
13keep_open for browser.open
12local/files and 'local:<name>'
11Browser failure snapshots, b.snapshot
10Streamed URL downloads in b.upload
9GA4 in the Google connection
8prompts/ and user/prompts/; local/ask
7Tasks tasks/<id>.md; schedules can run tasks
6ctx.locale
5local/upload and local/secrets for pages
4Prompt schedules; the open-chat message
3filter / order_by / cursor in query; ctx.db.aggregate
2ctx.browser, ctx.oauth, schedules
1Local functions and ctx.db, fetch, html, secrets, llm, mcp; tables; run / push; template upgrades

Render diagnostics