OpenCode Code Mode explained: JavaScript instead of repeated tool calls

By

Tagged: AI

Learn how OpenCode Code Mode combines tool calls in JavaScript, when batching helps, and where permissions, errors, and side effects still need care.

Code Mode lets an OpenCode agent write a short JavaScript program that calls tools and processes their results.

Say you want to compare several records from a service. With individual tool calls, the model asks for one record, reads the response, then asks for the next. With Code Mode, it writes the repeated work as a script and gets back only the comparison it needs. The service receives the same requests, but less intermediate data goes back to the model.

Code Mode is part of OpenCode v2. We’ll compare the two approaches with a small catalog example, then look at what I would check before using the pattern for operations that change data.

A tool call is a request for an operation

A model doesn’t have direct access to your filesystem or a service account. The coding application exposes tools with names, descriptions, and input schemas.

The model can request a call such as “read this file.” The application executes the tool and supplies the result to the conversation. The model uses that result to decide what happens next.

This works well when every result requires a decision. Read the failing test, inspect the implementation, then choose a repair: each step depends on what the previous one found.

Repeated independent reads are different. If we already know we need four records, there’s nothing to decide between the first request and the second.

The same job, two ways

Imagine a catalog tool named get_product. Its input is a product ID, and it returns a record with id, name, and stock.

We want to know which of four products have fewer than five units available. Here are four made-up records:

ID Product Stock
notebook Notebook 18
pencil Pencil 3
mug Mug 0
bag Bag 12

A sequential agent workflow can look like this:

Model: call get_product({ id: "notebook" })
Tool: { id: "notebook", name: "Notebook", stock: 18 }

Model: call get_product({ id: "pencil" })
Tool: { id: "pencil", name: "Pencil", stock: 3 }

Model: call get_product({ id: "mug" })
Tool: { id: "mug", name: "Mug", stock: 0 }

Model: call get_product({ id: "bag" })
Tool: { id: "bag", name: "Bag", stock: 12 }

Model: Pencil and Mug have fewer than five units.

Some models and applications can already request several ordinary tools in one response. Code Mode adds a way to express the processing and control flow too, including later calls that depend on earlier results.

In Code Mode, tool names and return shapes come from the tools your session discovers. If the catalog exposed our made-up tool as tools.catalog.get_product, a script could do this:

const ids = ['notebook', 'pencil', 'mug', 'bag']
const products = await Promise.all(
  ids.map(id => tools.catalog.get_product({ id }))
)

return products
  .filter(product => product.stock < 5)
  .map(({ id, stock }) => ({ id, stock }))

The result contains pencil and mug, without the full records for the products we aren’t interested in.

Fig 1 · 1/3Put the loop beside the tools
The model describes a short program for the operation.

What this can save

In the sequential example, the model makes four calls over several turns. The script puts those known operations together, so there are fewer model round trips. It also keeps a large response out of the conversation when the next step needs only a small part of it: a tool could return dozens of fields, while the script returns two.

The catalog still gets four get_product requests. If the service offers a bulk lookup, one call to that is better than four parallel single-record lookups. If speed is the reason you’re using it, measure the complete task, because the saving depends on the service and the script.

Some steps need no model turn in between. A script can fetch a list, take the IDs of matching records, and read their details, because the rule fits in JavaScript. Asking the model to apply the rule to every record adds nothing.

Try the data processing locally

We can check the filtering logic without connecting a catalog service, which also pins down the input and expected output before handing the work to an agent.

Save this complete example as catalog-demo.mjs:

import assert from 'node:assert/strict'

const products = new Map([
  ['notebook', { id: 'notebook', stock: 18 }],
  ['pencil', { id: 'pencil', stock: 3 }],
  ['mug', { id: 'mug', stock: 0 }],
  ['bag', { id: 'bag', stock: 12 }]
])

const tools = {
  catalog: {
    async get_product({ id }) {
      const product = products.get(id)
      if (!product) throw new Error(`Unknown product: ${id}`)
      return product
    }
  }
}

async function lowStock(ids) {
  const found = await Promise.all(
    ids.map(id => tools.catalog.get_product({ id }))
  )

  return found
    .filter(product => product.stock < 5)
    .map(({ id, stock }) => ({ id, stock }))
}

const result = await lowStock(['notebook', 'pencil', 'mug', 'bag'])

assert.deepEqual(result, [
  { id: 'pencil', stock: 3 },
  { id: 'mug', stock: 0 }
])

await assert.rejects(lowStock(['missing']), /Unknown product/)
console.log(result)

Run it with:

node catalog-demo.mjs

It runs in Node.js with fake tools, so it checks the part we control (which records qualify and how a missing record fails), not OpenCode itself.

What the OpenCode runtime can access

OpenCode exposes Code Mode through its execute tool. Its interpreter implements a JavaScript-like language with a supported subset of the APIs you know from JavaScript. The tool documentation describes a JavaScript runtime that calls tools from the session’s catalog.

It has no direct filesystem access, imports, timers, or fetch. You don’t install a package from inside a Code Mode script. To read a file or request a resource, the script needs an available tool that performs that operation.

That’s why a regular Node script may not run unchanged. Our local demo uses import, assertions, and console.log, which belong to the test harness. The shorter script above is the part the agent would write in Code Mode.

The release announcement shows Code Mode discovering tools and composing them through a tools namespace. MCP servers are included in this workflow by default in v2.

A catalog entry still has a contract. Read whether the returned value is a record, a list, or an envelope containing text. An agent can’t reliably write product.stock if the tool returns a string it first needs to parse.

Ask for a result rather than a guessed script

In OpenCode, connect only the integrations needed for the task. Then describe the bounded read you want:

Use the connected catalog tools to check these products:
notebook, pencil, mug, bag.

Use Code Mode for the independent reads if the tools support it.
Return only the IDs and stock counts below 5.
Report failed lookups separately. Don't change inventory.

Let the agent discover the actual tool signatures. A script copied from another integration can have the right structure and the wrong argument names.

Inspect the generated program and its tool activity. Check the requested IDs and how it handled missing records. Look at the result size too: returning the entire catalog would defeat our filter.

The final answer alone can’t tell you whether it queried the service, because a model can produce plausible stock counts without making any calls.

Parallel work needs a limit

Four independent reads are a reasonable demonstration of Promise.all(). Applying that pattern to a list of ten thousand IDs can overwhelm the service or hit rate limits.

For a larger job, prefer a batch endpoint if one exists. Otherwise process a bounded number of requests at a time. The service’s concurrency limits should determine that number.

Don’t parallelize operations that depend on each other. If one call creates an object and the next needs its ID, wait for the creation result first.

The same applies to operations on a shared record. Reading stock and updating it in separate calls doesn’t become an atomic transaction because both calls appear in one script. Another process can change the stock between them.

A failed script can leave completed operations

Promise.all() rejects when a promise rejects. It doesn’t undo successful calls or cancel every request already underway.

That’s manageable for read-only catalog lookups. If one lookup fails, you can report the missing result or retry it under the service’s rules. Promise.allSettled() can help collect per-call outcomes when the runtime supports the methods your script uses.

For operations that create records, the consequences are different. Suppose a script creates four issues and the fourth request fails. Retrying the whole script can duplicate the first three.

Use the service’s idempotency mechanism when available. Otherwise retain the created IDs and resume deliberately. A tool that accepts an operation key and rejects duplicates gives the agent a safer contract than “create this again and hope.”

This is also a reason to keep discovery, preview, and mutation separate. First return the exact proposed changes. Then approve that set before executing it. Avoid regenerating an open-ended target list during the write step.

Recover a partially completed scriptIf four issue-creation calls fail partway through, retrying everything can duplicate completed work. Retain created IDs and use idempotency or deliberate resumption.YesNoCreate four issuesFourth call failsKeep IDs ofcompleted issuesService supportsidempotency?Retry usingoperation keysResume only missingwork

Permissions still apply

Permission to use execute controls access to Code Mode. It doesn’t grant blanket access to every operation reachable through the catalog.

Nested built-in tools and MCP tools still apply their permission checks. Custom plugin tools need to assert the permissions appropriate to their operations. How to package and publish an OpenCode plugin covers shipping those tools as an npm package. The interpreter’s lack of fetch doesn’t make an unrestricted shell tool safe.

When reviewing a script, follow what each tool can do. A short call to a deployment tool can change production. A longer script containing only bounded reads may have a smaller impact.

Untrusted service content also stays untrusted. A product description telling the agent to export customer data isn’t a new instruction from you. Returning fewer fields means the model reads less of it, and you still need that boundary.

How I would use it

I would use Code Mode for repeated reads with a clear filter, or for collecting a small answer from several independent sources.

My Mac apps, like Note Repo, CLI Tools Cabinet and Tranquillity Maker, each ship from a GitHub repository’s Releases page. With a GitHub integration connected in OpenCode, I would ask for the latest release of each repository and get back only the names and version numbers, instead of watching the agent read every release page one turn at a time. “Find the failed builds for these repositories and return their URLs” has the same shape.

I would keep ordinary tool calls for exploratory work, where the next step depends on understanding the last result. A script that catches every error and carries on is the wrong tool when an unexpected error should change the plan.

I would also stop at reads. Publishing a release or creating issues from a script runs into the partial-failure problem above, and I’d rather approve those one at a time.

If I found myself running the same Code Mode job every week, I’d move the logic into a small program with its own tests, credentials, and retry behavior, and call that instead.

Want me to talk about your product? You can sponsor this site.

Related posts about ai: