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.
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:
| Path | Primitives |
|---|---|
local/*.ts | Local functions and ctx: tables, network, browser, shell, models, MCP, keys |
tables/<table>.json | Tables |
schedules/<id>.json | Schedules |
tasks/, prompts/, skills/, ANNULO.md | Tasks and notes |
annulo.json, user/ | Project manifest |
plugins/<id>/ | Plugins |
pages/ | Page endpoints |
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.
After the Shuttle → Annulo rename, the old names still work: shuttle.json / SHUTTLE.md / /_shuttle/ / X-Shuttle / the shuttle command / SHUTTLE_* env vars.
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
Deterministic, repeatable logic goes into local functions. Page buttons, schedules and the assistant call them directly, without the model.
- Put files in
local/<file>.ts; names match^[a-z0-9][a-z0-9_-]*$. Files starting with_(likelocal/_api.ts) hold shared code, imported by relative path; - Export
export function name(input, ctx), optionally async. It's called asfile.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
}- Bundled by esbuild to CommonJS (ES2017) and run in an embedded JS engine. No npm `require`;
talizenis type-only; - Globals:
fetch,console,URL(common fields andsearchParams.get/has),btoa/atob,TextEncoder/TextDecoder; - No
setTimeout; useawait ctx.sleep(ms); - A run lasts at most 60 minutes.
annulo run # 列出所有函数 / list functions
annulo run leads.stats --input '{"days":7}'
annulo run leads.stats --input @input.jsonProgress goes to stderr, the result to stdout. From pages, see Page endpoints. Logs: annulo logs --fn leads.stats.
ctx at a glance
The function's second argument. The "Cloud" column says whether it works in cloud functions.
| Member | Signature | What it does | Cloud |
|---|---|---|---|
ctx.db | see Reading and writing tables | query / aggregate / get / insert / update / delete | ✓ |
fetch | await fetch(url, { method, headers, body }) | Global fetch; no localhost or private networks. See Network | ✓ |
ctx.fetchAll | await ctx.fetchAll([url | { url, method, headers, body }]) | Up to 4 in parallel, results in input order | ✗ |
ctx.html | ctx.html(text) → .find(css) / .text() / .markdown() | Parse HTML | ✗ |
ctx.browser | see Browser | Drive local Chrome with your signed-in sessions | ✗ |
ctx.exec | await ctx.exec(cmd, args, { cwd?, input?, timeout?, env? }) | Run command-line tools. See Shell | ✗ |
ctx.secrets.get | (name) → string | null | Read Settings → Keys, or an env var | ✗ |
ctx.oauth | await ctx.oauth('google', { account? }) → access_token | Accounts authorized in Settings → Connections; ctx.oauth.accounts('google') lists them | ✗ |
ctx.mcp | await ctx.mcp(server, tool, args?) | Call a connected MCP tool; JSON results parsed. ctx.mcp.servers() shows status | creght only |
ctx.llm | await ctx.llm(prompt) or ctx.llm({ system, prompt }) → string | One 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.fetch | await ctx.llm.fetch(providerId, path, init) → Response | Call 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 CLI | no-op |
ctx.log | (...args) | Write to the run log | no-op |
ctx.sleep | await ctx.sleep(ms) | Up to 60 s | no-op |
ctx.locale | 'zh' | 'en' | UI language | ✓ |
ctx.workspace | object | project_id, site_id, offline, machine: { id, name }, logs_dir, annulo_api… | undefined |
ctx.chat_id | string | The 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.
Reading and writing tables
ctx.db reads and writes only declared tables. Calls are synchronous.
| Method | Returns |
|---|---|
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 })- where: field equality,
{ channel_id: 'x' }. A scalar also matches an item in an array field;where.idreads 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 previousnext_cursor; empty means done. Can't be combined with order_by or offset.
- 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>, andcountfor 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.
Network
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.
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(text) parses HTML: .find(css) returns [{ tag, text, attrs }], .text() gives plain text, .markdown() converts to Markdown. Invalid selectors throw.
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_openneedsshow: true;ctx.browser.profile(name) → { id } | null: whether the profile exists.
| Method | What 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.
Shell, keys, MCP, models
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;
cwdis relative to the project root and can't leave it; timeoutdefaults 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.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.
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.
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.
await ctx.llm('prompt')orctx.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 forkind === 'api'providers.
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).
You're on the subway and open the back office's Social page on your phone:
- 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; - 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; - 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.
| Called by | xhs.summary (cloud) | xhs.collect (remote) |
|---|---|---|
| A button inside Annulo | this computer | this computer |
| A button on the phone | creght, even with the computer off | relayed by creght → your computer (Annulo open) |
| A schedule | — | this computer |
The assistant via annulo run | this computer | this 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.
- Only project members can call them;
- Available:
ctx.db,ctx.locale,ctx.mcp('creght', …)(read-only, owner only),ctx.mcp.servers();progress/log/sleepare no-ops andworkspaceisundefined; - Using
secrets,fetchAll,html,oauth,browser,llm,execoragentthrows. That's whycollectabove can't becloud.
- 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.progressupdates stream back to the phone.
A function can't be in both cloud and remote.
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'sdb/<table>, and the assistant's tools alike; - Plugin tables are prefixed:
plugins/<id>/tables/t.jsonbecomes<id>_t; - System fields:
id,created_at,updated_at; json_schemadocuments the table for people and the assistant; Annulo doesn't validate writes against it.
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" }| Field | Meaning |
|---|---|
fn / task / prompt | Pick one: run a local function; run tasks/<id>.md; hand a prompt to the assistant in a new chat |
every | 30m / 6h / 1d / 7d; at least 5 minutes |
at | HH:MM, local time; wins over every when both are set |
input | Arguments for the function or task |
name | Display 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
fnschedules only; - Plugin schedule ids are
<plugin>/<id>, with fn / task written in full.
Tasks, prompts and notes
tasks/<id>.mdWork 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/<id>.mdThe 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/<name>/SKILL.mdInstructions for the assistant. Frontmatter has name (lowercase letters, digits, -, matching the folder) and description.
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.
Manifest and user/
{
"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": ["…"] }
}
}| Field | Meaning |
|---|---|
name / description | Template name and description, a string or { zh, en } |
min_annulo_api | Required capability version; older Annulo builds refuse to merge the upgrade |
plugins | { "<id>": "<repo>#<dir>" }, installed when a project is created |
assistant | The assistant panel: name, intro, suggestions (each a string or { zh, en }) |
To support Annulo builds below capability version 26, also ship a shuttle.json.
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.
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.
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.
| File | Meaning |
|---|---|
plugin.json | { name, description, min_annulo_api }; name / description can be { zh, en } |
PLUGIN.md | Notes 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.jsonin a repo, versioned by semver tags. Example: annulo/plugins.
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":…}}| Endpoint | What 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>/abort | Stop 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/running | Chats in progress; poll it to see when handed-off work is done |
GET local/schedules, POST local/schedules/<id>/run, …/toggle | Schedules: 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/upload | multipart field file, up to 200 MB → { url, … } |
POST local/files | Body 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,B | Write a key; check which are set (values never returned) |
POST fetch | Proxy 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 |
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.
The annulo CLI
Commands talk to the Annulo running on this computer; the assistant uses them in its shell too.
| Command | What 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 template | The project's template version and release notes |
annulo template upgrade [--to N] | Three-way merge to the latest (or a given) version |
annulo mcp list | Connected 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.
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.
| Tool | Parameters | What 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>.
Capability versions
| Version | Added |
|---|---|
| 33 | Offline upload URLs without a port, /_annulo/uploaded/…; annulo upload |
| 32 | ctx.chat_id |
| 31 | thinking in task frontmatter |
| 30 | ctx.mcp.servers(), ctx.agent.current(), provider kind |
| 29 | ctx.exec |
| 28 | Plugins |
| 27 | assistant in annulo.json |
| 26 | Annulo names (old names still accepted) |
| 25 | web_search flag on models |
| 24 | Streaming fetch bodies, TextEncoder / TextDecoder, arrayBuffer; ctx.llm.providers / ctx.llm.fetch; ctx.fetch removed |
| 23 | Multi-account ctx.oauth, ctx.oauth.accounts |
| 22 | The assistant on a phone; offline projects, workspace.offline |
| 21 | export const remote |
| 20 | export const cloud |
| 19 | ctx.db filter syntax, offset, total |
| 18 | Pages rendered locally, react-router |
| 17 | ctx.browser.profile |
| 16 | One file per table and schedule |
| 15 | workspace.machine |
| 14 | where.id; run logs, annulo logs |
| 13 | keep_open for browser.open |
| 12 | local/files and 'local:<name>' |
| 11 | Browser failure snapshots, b.snapshot |
| 10 | Streamed URL downloads in b.upload |
| 9 | GA4 in the Google connection |
| 8 | prompts/ and user/prompts/; local/ask |
| 7 | Tasks tasks/<id>.md; schedules can run tasks |
| 6 | ctx.locale |
| 5 | local/upload and local/secrets for pages |
| 4 | Prompt schedules; the open-chat message |
| 3 | filter / order_by / cursor in query; ctx.db.aggregate |
| 2 | ctx.browser, ctx.oauth, schedules |
| 1 | Local functions and ctx.db, fetch, html, secrets, llm, mcp; tables; run / push; template upgrades |