Ask for input from a Mastra tool

Suspend a tool for a confirmation or a value, and resume with the answer
View as Markdown

A Mastra tool asks the user for input by suspending. The adapter maps suspend() onto the platform’s form and maps the answer back onto resumeData, so the tool needs no platform-specific code.

Read Ask the user for input first for what the user sees, the actions they can take, and the behavior to design around. This page covers the Mastra side.

Write the tool

Add suspendSchema and resumeSchema to the tool, call suspend() where you want to pause, and branch on resumeData when the tool runs again.

import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const archiveRecordTool = createTool({
id: "archiveRecord",
description: "Archive a record. Asks the user to confirm before writing.",
inputSchema: z.object({ recordId: z.string() }),
suspendSchema: z.object({ message: z.string() }),
resumeSchema: z.object({
confirm: z.boolean().describe("Set true to archive. False cancels."),
}),
outputSchema: z.object({ archived: z.boolean(), message: z.string() }),
execute: async (input, ctx) => {
const resumeData = ctx?.agent?.resumeData as { confirm?: boolean } | undefined;
if (resumeData?.confirm === true) {
await archive(input.recordId);
return { archived: true, message: `Archived ${input.recordId}.` };
}
if (resumeData) {
return { archived: false, message: "Canceled. Nothing was archived." };
}
const record = await load(input.recordId);
await ctx.agent!.suspend({
message: `Archive "${record.name}"? This cannot be undone.`,
});
return { archived: false, message: "Waiting for confirmation." };
},
});

resumeSchema is the shape of the answer, so it is also the shape of the form. A confirmation is one boolean. Use a richer resumeSchema to collect a value, and the form grows the fields to match.

Three details matter.

resumeData is on ctx.agent, not on the context root.

The tool runs again from the top after the user answers, so check resumeData first. Treat anything other than an explicit confirmation as a decline.

The user sees the message field of your suspend payload. prompt, reason, and question also work. Put the specifics in it: name the record, not just its id, so the person is confirming the thing they think they are.

Give the agent durable storage

A suspended run is a snapshot. Without somewhere to persist it, the user answers and the resume fails with could not find a suspended run.

Register the agent on a Mastra instance with storage:

import { Mastra } from "@mastra/core";
import { LibSQLStore } from "@mastra/libsql";
export const mastra = new Mastra({
agents: { "my-agent": agent },
storage: new LibSQLStore({ id: "runs", url: "file:/data/agent-runs.db" }),
});

An agent’s Memory store does not cover this. Memory holds conversation history; suspended runs live in the workflows store on the Mastra instance.

Write to /data, the mounted volume, rather than to :memory:. A pending question has no timeout, so it may outlive a restart, and a file-backed snapshot survives one.

Next steps