Skip to content

Code Mode

For developers who connect a client to OpenBot or extend its server. The examples need the access stated on this page. Start with the developer manual.

In Code Mode, the Bot writes a short JavaScript program instead of calling App tools one at a time. The program calls the tools, works with their results, and gives back only what the Bot needs. This saves tokens, because large results stay in the program and do not go into the context of the model. The program can also call many tools at once.

Code Mode gives the Bot no new powers. Each App call in a program goes through the same capability check as a normal App tool call. Each call is also a normal App operation.

Word Meaning
Program The JavaScript code that the Bot writes. It is the body of an async function.
Binding One App capability that a program can call, such as apps.work_calendar.list_events.
Sandbox A closed place where the program runs. It has no network, no files, no timers and no Node.js.
Code run One program and the record of all its App calls. Read it with GET /v1/code-runs/:id.
Replay A new run of the same program after a decision. Calls that already ran give their saved result.

See Set up Code Mode.

The Bot gets two more tools. They are next to the normal App tools, and do not replace them.

Tool What it does
openbot_code_api Shows the TypeScript declarations of the bindings. The Bot can search them by words or by connection.
openbot_run_code Runs a program and gives back its result, its log lines, and a list of its App calls.

Each binding has two parts: the connection and the capability. A connection called “Work calendar” with a tool list-events becomes apps.work_calendar.list_events. apps.call("work_calendar.list_events", args) does the same. Resources and prompts become read bindings too. A program could look like this:

const [today, tomorrow] = await Promise.all([
apps.work_calendar.list_events({ day: "2026-10-23" }),
apps.work_calendar.list_events({ day: "2026-10-24" }),
])
const busy = [today, tomorrow].flatMap(
(result) => result.structuredContent.events,
)
console.log("events", busy.length)
return busy.filter((event) => event.attendees > 10).map((event) => event.title)

A tool call gives the program the result that the App sent. The program gets an Error when a call does not work:

Error name Why
AppCallDenied A decider did not allow the call.
AppCallFailed The App call failed. details.reason says why.
AppInputRequired The App needs more input. Answer the App operation in details.operationId outside the code.
VaultDenied The call has a Vault reference that the Bot may not use here. OpenBot sent nothing.

The program can catch these errors and go on.


Some App calls need a decision, for example a tool that writes. Then the program stops at that call. The Turn does not wait: the tool call of the program gets a pending result at once, and the Bot goes on. The request has the App call in app, and the program in code:

{
"capability": { "action": "apps.call", "target": "conn-7/send_email" },
"toolCallId": "call-9",
"app": {
"id": "…",
"callId": "call-9:1",
"arguments": { "to": "team@example.com" }
},
"code": { "run": "8f1c…", "index": 1, "code": "const notes = await …" }
}

After the decision, OpenBot runs the program again from the top, in the background. Each call that already ran gives its saved result, and OpenBot does not send it to the App again. The program then goes on past the call that waited. When it ends, its result comes back to the Chat as a notice, and the Bot goes on with it.

This works only when the program makes the same calls in the same order each time. So the program sees a fixed time in Date.now(), and Math.random() gives the same numbers in each run. If a program makes other calls than before, OpenBot stops it with ReplayMismatch. This can happen when a connection changed while the program waited. Nothing is sent by mistake.


A program can use secrets from a Vault without seeing them. The Bot gets vault next to apps. vault.github.token is a reference such as ⟦vault:github/token⟧, not the secret. OpenBot puts the secret in only when it sends an App tool call that the item allows. A call with a reference always needs a decision. The App result comes back with ⟦hidden:github/token⟧ in place of the secret.


GET /v1/chats/:chatId/code-runs lists the programs of a Chat. GET /v1/code-runs/:id reads one. Both use the current read permission of the source Chat. API keys and session cookies use the same check.

A code run has its business id and an executionId. Its status comes from the shared execution state. Observe or cancel it through the execution API. result holds the saved program output. A failure carries a safe category and error ID; the full cause stays sealed on the server.

calls lists each call in program order. Each entry has index, binding, app and rejection. app is the App execution view, with its own business ID, execution ID and saved result. Before that child exists, app is null. A call rejected before submission has a safe rejection name and message.

Code, arguments and call results stay sealed in the database. Waiting for an approval releases the current Turn’s wait through the shared decision log. The program keeps running as its own execution, and sends a notice when done.

After an upgrade, old programs keep their IDs, saved results, and App call links. You can read them in the same history API. A program with no saved caller or checkpoint ends as interrupted. A write with no known result stays unknown. Old programs do not run again when you read them.

reason Meaning
Error The program threw an error that it did not catch.
Timeout The program used all of its time.
Memory The program used all of its memory.
OutputTooLarge The program returned more data than outputBytes allows.
ReplayMismatch The program made other calls than the last time it ran.
Cancelled The program was stopped.
Unavailable OpenBot could not run or save the program, or Code Mode is off.