Skip to main content

Custom Tools

A guide to building and configuring custom tools in the thunk builder, including AI-based, code-based, and other tool types.

Custom Tools

A custom tool is a capability defined by the thunk builder that the AI agent can invoke during a workflow run. The AI agent calls the tool by name, passes structured inputs, and acts on the return value.

From + Add New Tool you can create several types, including Custom AI Tool, Custom Code Tool, Document Template Tool, Spreadsheet Lookup Tool, Custom Database Tool, and Tool Wrapper. You can also build a new tool to match one entry of an interface — a file you upload, or one already used by a plug-in connection on this thunk. Thunk fills in that tool's name, description, and inputs from the interface and leaves you to implement it; those three then stay fixed. See Reuse thunk interfaces and plug-in connections. This article focuses on AI and Code tools. For filling Word or Google Doc templates, see Generate documents from templates. For looking up rows in a spreadsheet by the columns you choose, see How to build a Spreadsheet Lookup tool. For querying a connected SQL database, see How to build a Custom Database tool. For wrapping an existing tool with fixed arguments and inputs of your own, see How to build a Tool Wrapper. To reuse a recent result when an AI or code tool is called again with the same inputs, see Cache results of a custom AI or code tool. Not sure which type fits? See the comparison in AI Tools.

An AI agent can build these tools for you through the FDA MCP, including their inputs, outputs, batch settings and the tools an AI or code tool may call. See Build thunks with the FDA MCP.

A tool is not finished until it has tests. When you build a tool, give it test cases, each with at least one assertion on what it should return. See Automated tests for custom tools.

An AI tool executes a natural language instruction. A code tool executes a JavaScript function. Both are built on the same tool page. What differs is the logic in the Details section of the Implementation tab, described below.


Tool Builder

Open Custom-built tools in the left nav and select a tool. On an MCP server thunk the page shows two lists. Exported tools are the ones published to external AI agents. Internal tools are helpers used inside the thunk, for example called by other tools. Manage export next to the Exported tools heading opens the screen where you choose which tools to publish.

A tool's page has these tabs:

  • Definition — the tool's name, description, and intent, and whether it is turned on.

  • Implementation — what the tool takes, what it returns, and how it does the work.

  • Try It — run the tool with sample values before you rely on it in a workflow.

  • Tests — saved test cases that check the tool after every change. See Automated tests for custom tools.

  • Call history — the tool's past runs.

Definition

Tool name is the snake_case identifier the AI agent uses to select the tool. The name should describe the decision, computation, or action the tool performs.

Description is the plain-language explanation of what the tool does and what it returns. The AI agent reads the description to determine when to call the tool. A precise description improves invocation accuracy. A vague description produces inconsistent behavior.

Logic/Intent describes, in plain language, what the tool should do and how. The AI that helps you build the tool uses it to design the inputs, outputs, and implementation. It is not the instruction or code the tool runs — that is on the Implementation tab.

The Enabled switch at the end of the tab decides whether the tool can be used inside this thunk: by its AI agents, its workflow steps, and its other tools. It does not decide whether the tool is published to external AI agents. That is set separately when you publish your thunk as an MCP server.

Implementation tab

The Implementation tab is split into sections you can open and close independently: Inputs, Outputs, Details, Tools, and Behavior. Only Details starts open. Each closed section shows a short summary in its header — for example Inputs (2 fields) or Tools (2 connections enabled) — so you can see what is set without opening it. Not every tool type has every section: Tools appears only for code tools and document template tools, and some tool types have no Outputs.

Inputs and Outputs each have a Show raw JSON schema? switch, so you can see either one as a JSON schema instead of a list of fields.

Inputs

Each input is defined as a named field with a description and a type. The AI agent reads the description of each input to determine what value to pass. Field names are case-sensitive and must match how they are referenced in the implementation.

A Conversation input lets a custom AI tool or a custom code tool read and message a conversation on the current work item. Use type Conversation, or a Reference that points at a Conversation property (the reference is the precise choice when a work item has more than one conversation). How the nested AI sees the transcript, how a code tool sends and waits, and examples of both are in How to build a tool that uses a conversation. While a custom AI or custom code tool is waiting for a reply, the calling step's activity log shows that tool's own log live — not only after the tool finishes.

A Reference input takes its type from a work item property, and follows that property if its type changes. Apart from the conversation case above, it sets only the type: the tool does not read the property's value from the work item. The AI agent that calls the tool passes the value, like any other input, so say in the input's description which property to pass. To have the platform pass the value itself — so the agent does not copy a large property into the tool call — map that field on the workflow step with Map I/O, or on a custom AI tool that calls this tool, onto that AI tool's own inputs. See Map a tool's inputs and outputs to the work item. A code tool sees only the inputs it is called with. If the agent leaves out a required input or passes the wrong type, the call fails before the tool runs, and the agent sees the error.

Prefer a Reference whenever an input carries the value of a work item property, and especially when the property has a structured type: a fixed list of values, a number, a date, a list or an object. The input then accepts exactly what the property accepts. For example, an expense tool whose category input is a Reference to an Expense Category property with the values Meals, Lodging and Ground transport refuses "Groceries" before the code runs, so the code never has to handle a category it doesn't know. When you add a value to the property, the tool accepts it too, with no second list to keep in step. Use a plain type only when no property holds the value.

The same goes for a Text property with a format, such as a length limit or a regular expression that the value must match (an order id like #W5061109 set as ^#W\d{7}$). A Reference input to that property, or a Text input that sets the regular expression itself, refuses a value in the wrong format. Before refusing a value that does not match the regular expression, Thunk.AI tries a few safe fixes: it removes spaces around the value, removes one pair of quotes around it, and adds the fixed text the format starts or ends with if the value is missing it. So W5061109 becomes #W5061109, and the tool receives the fixed value. A value that already matches is never changed. If no fix works, the call fails before the tool runs, and the agent sees which input was wrong, the value it sent and the format it needs, so it can correct the value and call again. Your code does not have to repair the format itself.

The same holds for an identifier the agent copies from one call to the next, such as an order number or customer id. Give it a property with a length limit, and make every input that carries it, in every tool, a Reference to that property. A value that lost or gained characters on the way is then refused before the tool runs, instead of reaching the wrong record. See "Values the AI agent copies" in Schematized State.

An input or output can also be a Structure Type, so a tool can take or return a whole structured record — a contact, an address, a list of line items — in one field instead of several loose ones. Define the shape once in the Type Library and the tool shares it with the columns and other tools that use it. See Structure Types and the Type Library.

Format constraints belong in the input description. If a date field expects DD/MM/YYYY, that constraint should appear in the description. If a field accepts only a fixed set of values, those values should be listed. If a field may be null and null has a specific meaning, that should be stated.

Outputs

Outputs declares what the tool returns, as named fields with a description and a type, the same way as inputs. Declared outputs tell callers what to expect. When your thunk is published as an MCP server, outputs declared as named fields are part of what it publishes for the tool, and a thunk that reaches this tool through a plug-in connection relies on them. A tool built to match an interface may add output fields to those the interface declares, but it must still return every one of them.

An AI tool needs at least one output field (see AI-based Tools Implementation). An AI Classifier's outputs are its judgments, so each must be Yes/No or a choice from a fixed list of values. A Document Template or Spreadsheet Lookup tool's output is set by its type.

Details

The Details section contains the logic for the tool. The form it takes depends on the tool type and is described in the sections below.

Tools

A code tool can call other tools. Turn on the connections it may use in the Tools section; its header shows how many are on. A custom AI tool's nested tools live under AI Instructions on the same page, including Map I/O onto that AI tool's own inputs and outputs. See Map a tool's inputs and outputs to the work item. A custom code tool does not map I/O.

Behavior

The Behavior section applies to all tool types:

  • Allow batch processing? lets the AI agent send the tool several requests in one call. It is on by default. While it is on, Requests run at a time sets how many of those requests run at once.

  • Behavior hints for AI agents tell an outside AI agent how the tool behaves when your thunk is published as an MCP server: whether it is read-only, whether it can overwrite or delete data, whether it is safe to repeat, and whether it reaches outside systems or people. Agents such as ChatGPT and Claude use these hints to decide what they can call without asking first. Each hint is a switch that starts at a default; turn it on or off wherever you know how the tool behaves. The hints do not change approval or whether the tool is available.

  • Result caching (AI and code tools only) lets later calls with the same inputs reuse a recent result, so the tool does not run again. It is off by default. Turn Reuse identical lookups on only when the tool reads data and does not change anything — a cache hit also skips tools this tool would have called. Set how many seconds to reuse the result, and use Clear cache after you change the tool so stored results are not reused. The closed Behavior header says caches results while it is on. See Cache results of a custom AI or code tool.

Warning icons on a tool

An amber warning icon next to a tool's name means one of the tool's schemas is not strict. You see it in the Custom-built tools list and in the thunk's tool lists. The tool's Implementation tab shows the same warning as a banner above the Inputs section. Hover over the icon to see which schema it is about: the input schema (what the AI agent passes to the tool), the output schema (what an AI tool must fill in when it finishes), or both.

A strict schema pins down exactly what the AI may pass: every object is closed to extra fields, and every field is listed as required. When a schema is not strict, the AI is held to it less tightly. It can leave fields out or add fields of its own, which makes the tool's calls and results less reliable. The tool still runs; the warning is about reliability, not an error.

Click the icon, or View details in the banner, to open the details. For each schema that is not strict, the dialog lists what does not pass. Then fix it in one of these ways:

  • Fix it rewrites the schema for you. It closes every object to extra fields and marks every field as required. Afterwards, the AI agent must always pass a value for a field that used to be optional, so check that each one still makes sense, and say in the field's description what to pass when there is nothing to give.

  • Edit the schema by hand when the dialog says it cannot be fixed automatically. This happens when the schema explicitly allows extra fields, or combines schemas in a way strict mode does not support. Open Inputs or Outputs on the Implementation tab, turn on Show raw JSON schema?, and change it there.

  • Open the thunk the tool comes from when the dialog says it belongs to another thunk. A tool imported from another thunk can only be changed there.

  • Ask an Owner or Admin of the thunk when the dialog says you need that permission.

Some warnings cannot be cleared from the tool:

  • A tool from an MCP server has the schema the server declares, so it cannot be changed in Thunk.AI.

  • A Document Template tool's schema is read from its template document. Tables and optional sections in the template are read as not required, so the warning reflects how the template is read, not a mistake in it. No fix is offered, because a changed schema would no longer match the template and the tool would fail.

If the dialog says the schema is not valid JSON, the AI cannot call the tool at all. Open the raw JSON schema and correct it.

A warning sign in the Tests column is a different signal: the last test run had inconclusive results. See Automated tests for custom tools.


Naming and Description Style Guide

The tool name, tool description, and input descriptions are read by the AI agent at runtime. Poorly written metadata produces incorrect or inconsistent invocations regardless of how well the implementation is written.

Tool Names

Tool names use snake_case. The name should express what the tool does, not how it does it.

Example

✅ Good name

is_email_send_required, is_continued_tracking_required, final_go_live_check

❌ Bad name

check, calculate, process

Names that describe the question the tool answers or the computation it performs work well. Generic names that do not distinguish the tool from others in the same thunk do not.

Tool Descriptions

The tool description should state what the tool does and what it returns, in one or two sentences. The AI agent uses this description to decide whether to call the tool at a given point in the workflow.

Example

✅ Good description

Produce a JSON object representing the semantic and syntactic structure of the input document.

❌ Bad description

Text analysis tool.

Descriptions that spell out the tool's purpose and what it returns let the AI know what to expect when calling it. Vague descriptions that could apply to many tools produce inconsistent behavior.

Input Descriptions

Each input description should state what the field contains and any constraints on its value. Three categories of constraint matter most:

  • Format. If the value must follow a specific format, state it explicitly.

    • ✅ The submission date in DD/MM/YYYY format.

    • ❌ The submission date.

  • Allowed values. If the field accepts only a fixed set of values, list them.

    • ✅ The current status of the request. Expected values: "Pending", "Approved", "Rejected".

    • ❌ The current status of the request.

  • Null semantics. Each input has an Is Required option in the tool builder. If Is Required is enabled, the field will never be null and no null handling is needed. If Is Required is not enabled, the field may be null. When null has a specific meaning, state it in the description.

    • ✅ The date the record was closed. Null if the record is still open.

    • ❌ The date the record was closed.

Input names use PascalCase by convention when they correspond to fields in the thunk's data schema. This makes it easier to trace which schema field each input maps to.


How the AI Agent Calls a Custom Tool

The AI agent decides to call the tool based on the tool name and description. It constructs the input object by reading each input's description and populating it from available data. It then receives the return value and uses it in subsequent steps.


AI-based Tools Implementation

An AI tool implements its logic as a natural language instruction. The AI agent reads the instruction and produces a result. AI tools are appropriate when the task requires language understanding, judgment, or interpretation that cannot be expressed as fixed rules.

In addition to Inputs, an AI tool requires an Outputs section. Outputs are defined the same way as inputs: each output is a named field with a description and a type. The AI agent reads the output definitions to understand what values it is expected to produce. A well-written output description tells the agent both what the field contains and the format it should be in. Without defined outputs, the agent has no schema to produce values against.

The instruction can refer to inputs by name. The tool's AI agent receives the input values as a JSON object alongside the instruction, together with the input definitions; a name in the instruction is not replaced with its value. The instruction is prose that describes what to evaluate, what to produce, and how to handle edge cases.

When the input is unstructured or the outcome requires interpretation, the logic cannot be expressed as code. produce_structured_document in the Benchmark Invoice Processor sample illustrates this. It extracts structured fields from an invoice.

Tool name:produce_structured_document

Inputs:

Name

Description

Type

fileUrl

URL of the document being processed.

TEXT

scenario

The scenario the document is part of, including descriptions of the entities involved and their roles.

TEXT

expectedLanguage

The language the document is expected to be written in.

TEXT

Outputs:

Name

Description

Type

DocumentStructure

A JSON object capturing all fields and values extracted from the source document, preserving the document's original structure and terminology.

TEXT

The thunk reads each named output field from the agent's response and makes it available to subsequent steps.

Read the document and produce a structured JSON object that captures all relational
information. Extract text exactly as it appears in the source document. Do not
modify values to any canonical form — if a field has a currency symbol, keep it;
if it has numerical punctuation, keep it. If a field is not present in the document,
return null for that field rather than inferring it from other values.

The instruction defines not just what to extract but how to behave when the document is incomplete or inconsistent.


Code-based Tools Implementation

A code tool is appropriate when the logic is rule-driven and the outcome is fully determined by the inputs. Use code when the decision can be expressed as conditions, comparisons, or calculations, and when the same inputs should always produce the same output. If the task requires reading unstructured text, exercising judgment, or interpreting ambiguous content, use an AI tool instead.

For the JavaScript format, available globals (getTypedInput(), tools, thunk.dates, openFile, and others), and how to return a result, see How custom code tools work.

A common shape is a single async function:

async function run(input) {
  return result;
}

The function is declared async; its name does not matter. You can also write only the function body, which is the preferred form: see How custom code tools work. The input parameter is an object whose keys correspond to the input names defined in the Inputs section. No external imports are available. Standard JavaScript and modern syntax are supported, including optional chaining, destructuring, and template literals.

Input fields can be null even when they are expected to have values. Guard against null inputs using optional chaining (?.) or fallback expressions (|| ""). Normalize string inputs with .trim() before comparison to avoid mismatches caused by whitespace. Guard clauses at the top of the function exit early when preconditions are not met, keeping the main logic path uncluttered.

Answer "which one" with a query tool

People often describe what they want by comparing it with the alternatives: "the cheapest one", "the largest size", "the same color as the others", "my most recent order". A tool that only filters a list (returns everything in stock in size M) leaves the comparison to the AI agent, which then tells the person that something is the cheapest without having worked it out.

Give the agent a tool that runs the whole query and returns the answer:

Part of the query

Example input

What to choose from

productId

Which ones qualify

filters: size M, in stock

Which one to pick

select: "cheapest", "most expensive" or "closest to current"

How to break a tie

Fixed in the code, such as the lowest item id

The tool returns the chosen item and the values it compared, so the agent can tell the person why. A code tool is the natural fit: the same query always gives the same answer. Pair it with a direction that every number, comparison and action the agent states to the person comes from a tool result; see How to Write Effective AI Instructions.

When you review a thunk, look for superlatives in its directions and tool descriptions ("the cheapest", "the latest", "the best match") and check that a tool computes each one.

Example: Schedule Wait Gate

A common pattern in scheduled workflows is a tool that decides whether a workflow instance should wait for the next scheduled run or conclude. The tool checks whether the item has reached a terminal status and returns a value the AI agent uses to route the workflow.

Basic Version

Tool name:is_continued_tracking_required

Inputs:

Name

Description

Type

CurrentStatus

The current status of the item. Expected values: "In Progress", "Pending Review", "Completed", "Cancelled", "Closed".

TEXT

async function run(input) {
  const terminalStatuses = ['Completed', 'Cancelled', 'Closed'];
  const status = (input.CurrentStatus || '').trim();
  if (terminalStatuses.includes(status)) {
    return 'NO';
  }
  return 'YES';
}

'YES' signals that tracking should continue; 'NO' signals that the workflow instance can conclude.

Enhanced Version

The tool can be extended to return a structured object when downstream steps require additional data.

Tool name:is_continued_tracking_required

Inputs:

Name

Description

Type

CurrentStatus

The current status of the item. Expected values: "In Progress", "Pending Review", "Completed", "Cancelled", "Closed".

TEXT

StartDate

The date the item was opened, in DD/MM/YYYY format.

TEXT

CurrentDate

Today's date, in DD/MM/YYYY format.

TEXT

Two new inputs provide the dates needed to compute how long the item was open. On conclusion, the AI agent uses FinalStatus and DaysOpen to update the record before the workflow concludes.

async function run(input) {
  const terminalStatuses = ['Completed', 'Cancelled', 'Closed'];
  const status = (input.CurrentStatus || '').trim();  if (!terminalStatuses.includes(status)) {
    const result = { continueTracking: true };
    return result;
  }  let daysOpen = null;
  if (input.StartDate && input.CurrentDate) {
    const [d1, m1, y1] = input.StartDate.split('/');
    const [d2, m2, y2] = input.CurrentDate.split('/');
    const start = new Date(y1, m1 - 1, d1);
    const end = new Date(y2, m2 - 1, d2);
    daysOpen = Math.round((end - start) / (1000 * 60 * 60 * 24));
  }  const result = {
    continueTracking: false,
    FinalStatus: status,
    DaysOpen: daysOpen
  };
  return result;
}

When the item is still active, the function returns early with continueTracking: true. When the item has reached a terminal status, it computes DaysOpen and returns the outcome as a structured object. The AI agent reads the object fields by name and uses them independently.


Custom Tool Field Contracts (FDA API and FDA MCP)

The rules for batch_tools fields whose schema descriptions only
summarize them.

Response parsers (api and db tools). A responseParser is an
optional JavaScript snippet run in an isolated sandbox as the body of an
async function. Call getResponse() for the raw response text — an api
tool's HTTP body, a db tool's CSV result; there is no response
variable. input holds the tool's input arguments, log(str) appends to
an execution log returned with the result, and
​await fetch_str(url, options) performs one extra HTTP request and
returns its body text (a non-2xx status throws; private addresses are
blocked). The snippet must return a string and has a 5-second timeout.
String literals in the snippet must not contain raw newline characters —
build a newline with String.fromCharCode(10).

Output declarations (outputSchema / outputProperties). Declare
what a code, api, db, ai or classifier tool returns with one of
the two, not both: outputSchema is JSON Schema (an object of named
fields, or a bare value such as {"type":"string"}), and
​outputProperties is the batch_properties field shape, stored and
returned as outputSchema (set constraints.required on a field the
tool always returns; otherwise it may be null). Omitting both on an
update keeps the stored output; {} or [] clears it. A code tool's
return value is checked against the declaration, an ai tool's nested
agent must conclude with it, and a classifier needs at least one Yes/No
or text-enum field. A tool pinned to an interface may add output fields
but never remove or retype one the stored output declares.

Result caching (cachePolicy, clear_cache). On an ai or code tool, cachePolicy turns on reuse of identical lookups (cacheable) and how long to keep them (defaultTtlSeconds, 1–604800 seconds; 86400 is one day). Only enable this when the tool reads data and does not change anything — a reused result does not run the tool or call nested tools. Changing the instruction or code does not forget stored results; clear_cache (same name, no other fields) makes the next identical lookup run again and does not change cacheable or the duration. See Cache results of a custom AI or code tool.

Partial updates (intent, description, title, codeEdits, guidanceEdits, cachePolicy, addSchemaField, removeSchemaField). An update can change one part of a tool without sending the rest. Do not mix a partial update with tool. intent, description, and title replace those stored strings on any custom tool; omit a field to keep it. For a code tool, codeEdits is a list of find and replace texts applied in order to the saved code; each find must appear exactly once. guidanceEdits is the same kind of list applied to an ai tool's saved userGuidance (ai tools only). cachePolicy on the update (not inside tool) sets cacheable and/or defaultTtlSeconds and keeps the stored values you omit. addSchemaField / removeSchemaField add or remove one top-level field (name, and for add a JSON Schema schema) on the input or output (side; omit side for input) of any tool that authors its schema — not documentTemplate or spreadsheetLookup. Send addSchemaField or removeSchemaField, not both, and do not mix codeEdits with the other patch fields. A tool pinned to a Workflow Plan step cannot change a schema field this way — change the step, or detach the tool first. This does not forget cached results; use clear_cache for that.

Nested tool access (toolConfig). On an ai or code tool,
​toolConfig takes the enable/disable library and tool operations a
step's toolConfig uses (enable_library, disable_library,
​enable_tool, disable_tool) and controls which AI Tool Libraries and
tools the implementation may call — the ai tool's nested agent, the
​code tool's tools.* calls. It does not take set_io_bindings (Map
I/O is a workflow-step setting; see Map a tool's inputs and outputs to
the work item
)
or set_binding_column. A tool inherits the thunk's enabled libraries
unless it disables them, an enable_* operation also opts the thunk in,
and the operations apply on top of the stored config. Read the effective
result from the tool summary's toolConfig (includeImpl /
​includeToolImpl). Plug-in binding inputs are set with bindingCases,
not here.

Step pinning (stepTarget). stepTarget pins a tool to one Workflow
Plan step (by id and/or title) and copies that step's current input and
output bindings onto the tool as ordinary schemas. Later updates may
change those schemas; the step's settings warn when they no longer match.
It does not set the step's implementation — use batch_steps
​implementationTool for that. Sending null detaches the tool, and
​stepTarget cannot be combined with signature.

Document templates. Every documentTemplate add or update fetches
the template variables from the document into the input schema's data
(structure from the template; descriptions and narrowed types from what
you send), so after editing the template document, resend the tool as an
​update to refresh them. Google Drive and OneDrive/SharePoint URLs need
Google Drive or Microsoft 365 enabled on the thunk; a .docx stored on
the thunk through upload_files or create_upload_link needs no extra
connection, and arbitrary internet URLs are not supported. The template
document itself must already exist.

Examples in the Sample Library

  • Benchmark Invoice Processor — uses an AI tool, produce_structured_document, to read an invoice and return a structured JSON object capturing all relational data.

  • Lead Enrichment for Gyms — uses an AI tool, classify_image, to evaluate gym photos and categorize them based on content rules defined in the instruction.

Did this answer your question?