ToolResult
JavaScript · package toolnexus · SPEC §1 · js/src/types.ts
interface ToolResult { output: string isError: boolean metadata?: Record<string, unknown>}What every execute returns. Three fields, and the whole tool-calling loop is built on them:
output is the text handed back to the model, isError says whether the call failed, and
metadata is free-form — except for one reserved key that turns a result into a suspension.
When to use it
Section titled “When to use it”Every time you write a tool. It is the return type of
Tool.execute, so you construct one on every code path — success,
failure, and everything in between.
Why an error flag and not an exception
Section titled “Why an error flag and not an exception”output is always a string — it is what the model reads. Serialize structured data yourself
(JSON.stringify) rather than expecting the loop to do it, so you control exactly what the model
sees.
Examples
Section titled “Examples”1. Success and failure on the same tool
Section titled “1. Success and failure on the same tool”import assert from "node:assert"import type { ToolResult } from "toolnexus"
function readConfig(key: string): ToolResult { const config: Record<string, string> = { region: "eu-west-1" } if (!(key in config)) { // Recoverable: the model can read this and try another key. return { output: `No such config key: ${key}`, isError: true } } return { output: config[key], isError: false }}
const found = readConfig("region")assert.equal(found.output, "eu-west-1")assert.equal(found.isError, false)
const missing = readConfig("nope")assert.equal(missing.isError, true)console.log("ok:", found.output, "|", missing.output)2. Structured output and metadata
Section titled “2. Structured output and metadata”output must be a string, so serialize deliberately. metadata rides alongside for your code —
the model never sees it, which makes it the right place for bookkeeping.
import assert from "node:assert"import type { ToolResult } from "toolnexus"
function search(q: string): ToolResult { const hits = [ { id: 1, title: "Getting started" }, { id: 2, title: "Advanced usage" }, ] return { // The model reads this. Make it legible, not just valid. output: hits.map((h) => `#${h.id} ${h.title}`).join("\n"), isError: false, // Your code reads this. The model never sees it. metadata: { title: `search: ${q}`, count: hits.length, ids: hits.map((h) => h.id) }, }}
const res = search("usage")assert.equal(res.metadata?.count, 2)assert.deepEqual(res.metadata?.ids, [1, 2])assert.ok(res.output.includes("Advanced usage"))console.log("ok:", res.metadata?.title)3. The reserved key — metadata.pending is a suspension
Section titled “3. The reserved key — metadata.pending is a suspension”metadata is free-form with one exception. A pending key holding a Request means “this tool
cannot finish until something out-of-band happens” — the loop parks the run instead of returning.
You rarely write this by hand; pending builds it for you.
import assert from "node:assert"import { pending, authRequired, pendingOf } from "toolnexus"
// pending() returns a ToolResult carrying metadata.pending = Request.const res = pending({ kind: "input", prompt: "Which environment?" })
assert.equal(res.isError, true) // a parked call is not a successconst req = pendingOf(res)assert.ok(req, "pendingOf reads the suspension back off the result")assert.equal(req?.kind, "input")assert.equal(req?.prompt, "Which environment?")assert.ok(req?.id, "an id is generated as the correlation key")
// authRequired is sugar for the login case.const auth = authRequired("https://example.com/login")assert.equal(pendingOf(auth)?.kind, "authorization")assert.equal(pendingOf(auth)?.url, "https://example.com/login")
// An ordinary result has no suspension.assert.equal(pendingOf({ output: "done", isError: false }), undefined)
console.log("ok:", req?.kind, "|", pendingOf(auth)?.kind)Fields
Section titled “Fields”| Field | Type | What it is |
|---|---|---|
output |
string |
The text handed to the model. Always a string — serialize structured data yourself. |
isError |
boolean |
Whether the call failed. Fed back to the model, not thrown. |
metadata |
Record<string, unknown> |
Free-form, for your code. Reserved: pending holds a §10 Request. |
See also
Section titled “See also”Tool— what returns thisToolContext— whatexecutereceivespending— build a suspending resultpendingOf— read a suspension back off a result