Skip to main content

How to build a tool that uses a conversation

Give a custom AI or custom code tool a Conversation input so it can read a work-item conversation, send messages, and wait for replies.

How to build a tool that uses a conversation

A custom tool can take a Conversation as an input. When that tool runs on a work item, it can read that conversation and send messages on it — the same thread a workflow step would use.

Use this when a step should hand the chat (or an email thread) to a tool that owns the back-and-forth: collect a list of answers, walk someone through a fixed script, or read what they said and reply.

  • A custom AI tool is the right fit when the questions or the interpretation can vary. The tool's own AI reads the transcript and decides what to send.

  • A custom code tool is the right fit when the script is fixed. Your JavaScript sends exact messages and waits for exact replies.

Naming, input descriptions, and the rest of the tool builder are in Custom Tools. The JavaScript contract — format, globals, and how to return a result — is in How custom code tools work. This article is only the Conversation input.

The conversation itself is a Conversation property on the work item. Chat apps usually name it EndUserConvo. See Set up the hosted Chat UI and the conversational REST API.

Add the Conversation input

  1. Open the tool under Custom-built tools.

  2. In Inputs, add a field.

  3. Set its type to Conversation, and name it after the work item's Conversation property (often EndUserConvo).

  4. If the work item has more than one Conversation property, prefer type Reference and point it at the exact property. That names the thread even when several conversations sit on the same work item.

The tool only gets send-and-read access when it runs as part of a work item that has that conversation. Try it! and a direct Tools API call have no work item, so those send tools are not there. Run the tool from a workflow step, the hosted Chat UI, or the conversational REST API.

When you add the tool with the Builder API or Builder MCP, mark the input as a Conversation — not a plain string. The stored input is a string whose format is conversation-uri. A format of conversation is accepted and stored as conversation-uri. A plain string input does not get add_message_to_<property>.

You do not turn conversation tools on under the tool's Tools section. They appear because of the Conversation input.

The input binds to a Conversation property on the current work item when:

  • the input's name matches a Conversation property, or

  • the value passed in is that conversation, or

  • the work item has exactly one Conversation property.

If none of those apply, the tool still runs, but it cannot send on a conversation.

Waiting for a reply only works when that conversation can receive replies. If you turned on Disable replies on the Conversation property, you can still send, but the tool cannot wait for an answer.

Custom AI tools

When the nested AI runs, two things happen:

  • The Conversation input's value is the current transcript — what has been said so far.

  • The nested AI also gets add_message_to_<property>, the same send tool a workflow step has for that conversation. For a property named EndUserConvo, the tool is add_message_to_enduserconvo.

In the tool's instruction, tell the nested AI when to send, when to wait, and when it is allowed to finish. To wait for an answer, send with waitMode set to waitForReply. Do not let it finish until every required answer appears in what the person actually said — a short reply is not by itself a reason to stop.

If the nested AI waits for a reply, that wait belongs to the tool. The calling step stays paused until the tool finishes.

An AI tool imported from another thunk's library works the same way: it reads and messages the calling work item's conversation, and a wait for a reply resumes that imported tool. See Modular reuse of tools.

Example: collect onboarding answers

Tool name: collect_onboarding_answers

Inputs:

Name

Description

Type

EndUserConvo

The end-user conversation on this work item.

Conversation

questions

The questions to ask, in order, one per line.

TEXT

Outputs:

Name

Description

Type

answers

Each question mapped to the person's reply. Use only words they sent.

TEXT

Instruction:

You are collecting answers on the EndUserConvo conversation.

Read the transcript in EndUserConvo. For each line in questions that does not
yet have an answer the person actually said, send that question with
add_message_to_enduserconvo and set waitMode to waitForReply. Ask one
question at a time.

Do not invent or skip an answer. Do not finish until every question has a
reply from the person. A short confirmation such as "ok" is not an answer
to a later question.

Return answers as JSON: each question mapped to the exact reply.

The workflow step that calls this tool passes the work item's conversation and the question list. The tool owns the back-and-forth; the step continues when answers comes back.

Custom code tools

Declare the same Conversation input (or a Reference to a Conversation property). The sandbox then has the send tool for that property on tools, plus a helper to read the transcript.

For a property named EndUserConvo:

await tools.add_message_to_enduserconvo({
  action: "send",
  addRecipientEmails: [],
  subject: null,
  body: "What would you like help with?",
  waitMode: "doNotWait",
});

The send-tool name is the property name, lowercased and turned into a JavaScript identifier: EndUserConvo becomes add_message_to_enduserconvo. Do not enable conversation tools as libraries — they are not on tools that way.

Argument

What it does

action

Use "send" to deliver the message.

body

The message text.

waitMode

"doNotWait" sends and continues. "waitForReply" pauses the code tool until the person replies; the await then resolves to that reply.

addRecipientEmails / subject

Used on email conversations. For chat, pass [] and null.

Read what has been said so far with getConversationTranscript("EndUserConvo") when the script itself needs the earlier messages. Pass the exact property name (EndUserConvo), not the send-tool name (add_message_to_enduserconvo). The transcript is a snapshot: after a waitForReply resumes, a later call includes the new reply. Do not return the transcript as an output — the caller already has the conversation and can see the messages.

To call another custom tool that also takes a Conversation input, pass the Conversation field from getInput() or getTypedInput() — that value is the conversation itself, not the transcript:

await tools.other_tool({
  EndUserConvo: getInput().EndUserConvo,
});

Enable that other tool in this tool's Tools section so it appears on tools. The other tool must declare its own Conversation input (or a Reference to the Conversation property).

Example: ask a fixed list of questions

Tool name: ask_intake_questions

Inputs:

Name

Description

Type

EndUserConvo

The end-user conversation on this work item.

Conversation

const questions = [
  "What is your name?",
  "What company are you with?",
  "What is your role there?",
];

const answers = [];
for (const question of questions) {
  const reply = await tools.add_message_to_enduserconvo({
    action: "send",
    addRecipientEmails: [],
    subject: null,
    body: question,
    waitMode: "waitForReply",
  });
  answers.push({ question, reply: String(reply) });
}

return { answers };

The step that calls this tool only needs to pass the conversation. The script asks each question, waits, and returns the list. Same inputs always produce the same sequence — that is the point of a code tool.

Example: a short choice, then a follow-up

Use code when the next question depends on a fixed set of replies, not on judgment.

Tool name: triage_urgency

Inputs:

Name

Description

Type

EndUserConvo

The end-user conversation on this work item.

Conversation

function normalize(text) {
  return String(text ?? "").toLowerCase();
}

const first = await tools.add_message_to_enduserconvo({
  action: "send",
  addRecipientEmails: [],
  subject: null,
  body: "Is this urgent? Reply yes or no.",
  waitMode: "waitForReply",
});

const urgent = normalize(first).includes("yes");
const followUp = urgent
  ? "What happens if this slips today?"
  : "When do you need an answer?";

const second = await tools.add_message_to_enduserconvo({
  action: "send",
  addRecipientEmails: [],
  subject: null,
  body: followUp,
  waitMode: "waitForReply",
});

return {
  urgent,
  detail: String(second),
};

If the reply is free-form and you need to interpret it, use a custom AI tool instead.

Imported tools

A custom AI tool or custom code tool imported from another thunk still reads and messages the calling work item's conversation. If it waits for a reply, that imported tool continues when someone answers — the calling workflow stays paused until the tool finishes.

After it works

Try it! cannot exercise the send tools, because there is no work item. Confirm the back-and-forth from a real run — the hosted Chat UI, the conversational REST API, or a work item that already has the conversation.

For cases that do not need a live reply, add lasting checks on the Tests tab. See Automated tests for custom tools.

Did this answer your question?