OpenCode Code Mode explained: JavaScript instead of repeated tool calls

By

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.

Suppose you want to compare several records from a service. With individual tool calls, the model can ask for one record, read the response, and then ask for another. With Code Mode, it can describe the repeated work in a script and return the comparison it needs.

The service still receives requests. Code Mode changes how the agent organizes them and how much intermediate data needs to return to the model.

This feature is part of OpenCode v2. We’ll compare the approaches with a small catalog example, then look at what I would check before using the same 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, and then choose a repair. The second operation depends on what we discover in the first.

Repeated independent reads are different. If we already know that we need four records, there may be no useful decision to make between the first and second request.

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. These are invented records for the example:

IDProductStock
notebookNotebook18
pencilPencil3
mugMug0
bagBag12

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.

If the discovered Code Mode catalog exposes our illustrative tool as tools.catalog.get_product, a script can 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 }))

That script is an illustration of a custom catalog tool, not a built-in OpenCode product API. Real names and return shapes come from the tools your session discovers.

The returned result would contain pencil and mug. It doesn’t need to include the full records for 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 decides to make four calls over several turns. The script expresses those known operations together.

That can reduce model round trips. It can also keep a large response out of the conversation when the next step only needs a small part of it. A tool could return dozens of fields, while the script returns two.

It doesn’t reduce the number of catalog requests in our example. We still call get_product four times. If the service offers one bulk lookup, that might be a better choice than four parallel single-record lookups.

It also doesn’t establish a speed improvement of a particular percentage. Execution time depends on the service, the model, and what the script does. We would need to measure the complete task, including discovery and failed attempts, to make that claim.

There are cases where no model continuation is needed between known steps. For example, a script could fetch a list, take the IDs of matching records, and read their details. The decision follows a rule that fits in JavaScript. Asking the model to repeat the rule for every record adds little value.

Try the data processing locally

We can check the filtering logic without connecting a catalog service. This also makes the input and expected output clear 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

This is a Node.js demonstration of our algorithm with fake tools. It doesn’t test OpenCode’s interpreter, discover an MCP server, or measure a model’s performance. The assertions test the part we control: which records qualify and how a missing record behaves.

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.

This distinction explains why a regular Node script may not run unchanged. Our local demonstration uses import, assertions, and console.log. Those belong to the test harness around the algorithm. The shorter script above is the orchestration we want the agent to express.

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.

The agent should discover the actual tool signatures. A script copied from another integration can have the right JavaScript structure and completely 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. A model could 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.

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. 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 unnecessary fields can reduce what the model has to read, but it doesn’t replace that boundary.

Where 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. “Find the failed builds for these repositories and return their URLs” is a reasonable shape of task when the integrations provide suitable tools.

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 proceeds isn’t useful when an unexpected error should change the plan.

For a repeated production job, I’d move the proven logic into a maintained program or workflow. That gives me a place for tests, credentials, and deliberate retry behavior. Code Mode is convenient for composing tools during a conversation; a recurring business operation deserves a contract someone can maintain.

Tagged: AI · All topics

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

~~~

Related posts about ai: