Mastra
View as MarkdownMastra is a TypeScript framework for agents, tools, and workflows. The Dex adapter runs a Mastra agent inside a durable session. It is available for TypeScript.
When to use it
Use it when your agent already uses Mastra. Keep the agent, its tools, and its model router, and let Dex own the session.
How a session runs
Every agent tool, read tools included, goes to Mastra as a client tool. So Mastra stops after the model call with the tool calls. Each RunModel Step makes one model call.
Each tool call runs the agent's original tool once, in its own Step, in parallel: CallTool for a read or an idempotent write, CallSingleAttemptTool for any other write. A call that needs approval waits in AwaitToolApproval first. The next RunModel adds the recorded results and calls the model again.
A turn that looks up an order runs RunModel, CallTool, ResumeTurn, and RunModel.
Install
Install the adapter with its framework. Each adapter brings in the Dex Agents core.
npm i @superdurable/dex-agents-mastra @superdurable/dex-agents @superdurable/dex @mastra/core zod
Example: agents/typescript/mastra/README.md
The example
The example is the order support agent that every framework page uses.
The Flow type is MastraOrderSupportAgent. The full example is under examples/typescript/src/products/agent-harnesses.
Agent and tools
The tools are Mastra tools on a Mastra Agent. The tool policies name each tool.
export function createMastraOrderSupportFlow(
orderDesk: OrderDesk,
model: MastraModelConfig,
slack: Connection | undefined,
): AgentSessionFlow {
const lookUpOrder = createTool({
id: "look_up_order",
description: "Look up one order by its order ID and return its items and status.",
inputSchema: z.object({ order_id: z.string() }),
execute: async ({ order_id }) => orderDesk.describeOrder(order_id),
});
const cancelOrder = createTool({
id: "cancel_order",
description: "Cancel one order that has not shipped yet.",
inputSchema: z.object({ order_id: z.string(), reason: z.string() }),
execute: async ({ order_id, reason }) => orderDesk.cancelOrder(order_id, reason),
});
const agent = new Agent({
id: "order-support",
name: "OrderSupport",
instructions: orderSupportInstructions,
model,
tools: { look_up_order: lookUpOrder, cancel_order: cancelOrder },
});
return createMastraAgentSessionFlow(agent, mastraOrderSupportFlowType, {
toolPolicies: {
look_up_order: { effect: "read" },
cancel_order: { effect: "non_idempotent_write" },
},
toolSources: createSlackConnectorToolSources(slack),
});
}
Example: examples/typescript/src/products/agent-harnesses/mastra/mastra-order-support-flow.ts
Worker
One Worker serves the session Flows of every harness in the example. Build the same session Flow in every process that starts sessions or calls their RPCs.
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
// ...
// One order desk for the four agent harness examples; each harness registers its own session Flow type.
const orderDesk = new OrderDesk();
// ...
const mastraOrderSupportFlow = createMastraOrderSupportFlow(orderDesk, loadMastraOrderSupportModel(), slack);
Example: examples/typescript/src/registry.ts
Session routes
Every harness serves these same routes. Session routes explains them and the session token.
router.post("/sessions", async (request, response) => {
const initialMessage = optionalString(requireBody(request), "message");
// The server picks the session ID, so a caller cannot name another session.
const sessionId = `order-support-${randomUUID()}`;
await startAgentSession(client, flow, sessionId, { initialMessage }, { timeoutMs: 30 * DAY_MS });
response.json({ session_id: sessionId, session_token: issueSessionToken(sessionId) });
});
router.get("/sessions/:id/snapshot", async (request, response) => {
response.json(convertAgentSnapshotToJson(await client.invokeRPC(flow.getAgentSnapshot, sessionIdOf(request))));
});
router.post("/sessions/:id/messages", async (request, response) => {
const sessionId = sessionIdOf(request);
const message = { text: requireString(requireBody(request), "text") };
sendAccepted(response, await client.invokeRPC(flow.sendUserMessage, sessionId, message));
});
router.post("/sessions/:id/interrupts/:interruptId/approve", async (request, response) => {
const sessionId = sessionIdOf(request);
const approval = { interruptId: String(request.params.interruptId), approved: true, rejectionMessage: null };
sendAccepted(response, await client.invokeRPC(flow.resolveInterrupt, sessionId, approval));
});
Example: examples/typescript/src/products/agent-harnesses/order-support/order-support-routes.ts
Run it locally
Start Dex and the example
In a first terminal, render the session graph, then start Dex. Dex Web reads the graph when it starts, to show the session and its approval controls.
cd examples/typescript
npm install
mkdir -p definitions
npx dex-agents-session-graph --flow-type MastraOrderSupportAgent \
--permission order-support.operate --out definitions/mastra.json
dexcli dev --flow-rendering-dir ./definitions
Example: examples/typescript/src/products/agent-harnesses/README.md
dexcli dev stays in the foreground. In a second terminal, start the example:
cd examples/typescript
OPENAI_API_KEY=... MASTRA_TELEMETRY_DISABLED=1 npm start
Example: examples/typescript/src/products/agent-harnesses/README.md
The app serves on 127.0.0.1:8080. Dex Web is at http://127.0.0.1:8802.
The example has no demo model. Its model router reads the provider key, such as OPENAI_API_KEY, from the Worker's environment.
| Variable | Default | Meaning |
|---|---|---|
| DEX_MASTRA_MODEL | openai/gpt-5-mini | A Mastra model router ID. |
| OPENAI_API_KEY | none; required for the default | The router's provider key. |
| MASTRA_TELEMETRY_DISABLED | unset | Set to 1 to turn off Mastra's telemetry. |
| DEX_AGENT_SESSION_KEY | a random key | The key that signs session tokens. |
Call the session
Start a session, ask the agent to cancel order 1002, and approve the call:
base=http://127.0.0.1:8080/products/agent-harnesses/mastra
session=$(curl -s -X POST "$base/sessions" -H 'Content-Type: application/json' \
-d '{"message": "Please cancel order 1002"}')
id=$(echo "$session" | jq -r .session_id)
token=$(echo "$session" | jq -r .session_token)
until interrupt=$(curl -s "$base/sessions/$id/snapshot" -H "X-Agent-Session-Token: $token" \
| jq -re '.pending_interrupts[0].interrupt_id'); do sleep 1; done
curl -s -X POST "$base/sessions/$id/interrupts/$interrupt/approve" -H "X-Agent-Session-Token: $token"
Example: examples/typescript/src/products/agent-harnesses/README.md
The loop reads the snapshot until the agent asks to run cancel_order. That call waits for approval, then runs once. Read the snapshot again to see the agent's answer.
Approve in Dex Web
Instead of the last command, you can approve the call in Dex Web. Follow the steps in Approve in Dex Web.
Connect Connectors
The agent can call Connector operations as tools, as Connectors as tools describes. The example adds the Slack tool slack_post_thread_reply, which replies in a Slack thread.
Offer the tool
Install @superdurable/connectors-sdk and the Connector package, such as @superdurable/connector-slack. The install commands are in Connector tools.
The Worker opens the Slack connection support-slack from the project configuration. connectorTool turns the operation into a tool, and ConnectorToolSource holds it. Without the connection, the agent gets no Slack tool.
export async function openSlackConnectionFromProject(
environment: NodeJS.ProcessEnv = process.env,
): Promise<Connection | undefined> {
// ...
const project = await loadFromEnvironment(environment);
// ...
return newProjectConnection(project, slackConnectionName);
}
// ...
export function createSlackConnectorToolSources(slack: Connection | undefined): ToolSource[] {
if (slack === undefined) {
return [];
}
return [
new ConnectorToolSource([
connectorTool(slack.operations.postThreadReply(), slack.reference, {
manifest,
inputValidator: PostThreadReplyInputSchema,
// The manifest does not ask for approval; this agent asks before every post.
requiresApproval: true,
}),
]),
];
}
Example: examples/typescript/src/shared/slack-connector-tools.ts
The Worker opens the connection once, at startup, and every agent uses it:
// One Slack connection for the agent examples; without it they offer no Slack tools.
const slack = await openSlackConnectionFromProject();
Example: examples/typescript/src/registry.ts
The agent passes the tool source next to its own tools:
return createMastraAgentSessionFlow(agent, mastraOrderSupportFlowType, {
// ...
toolSources: createSlackConnectorToolSources(slack),
});
Example: examples/typescript/src/products/agent-harnesses/mastra/mastra-order-support-flow.ts
How a call runs
Each call of slack_post_thread_reply runs once, in its own CallSingleAttemptTool Step, after a person approves it. The Slack Connector tool explains why.
The adapter passes the Connector tools to Mastra as client tools without execute. A call never runs inside the model call. Mastra approval settings do not apply to it; the source decides approval. With tool sources, a toolPolicies entry never makes a tool one that Mastra runs itself; only mastraRunToolNames does.
Configure the connection
Connections explains where the Worker finds the support-slack connection and what the Slack app needs.
To try it, start the example with DEX_CONNECTOR_CONFIG_FILE set. Then ask the agent to reply in a Slack thread, with the channel ID and the thread timestamp:
Reply in Slack thread C0123456789 1710000000.000100: Order 1002 ships on Thursday.
The post waits for approval, like cancel_order.
Limits
- Tools Mastra runs itself, such as sub-agents, workflows, and Memory tools, cannot run in a Step. The adapter offers one only when toolPolicies names it as a read without approval. It then runs inside RunModel and repeats when that Step reruns.
- Mastra Memory is not used. Do not select a Memory thread in the agent's defaults or scopes.
- beforeToolCall and afterToolCall hooks run only for tools Mastra runs itself.
- A tool result reaches the model as text. toModelOutput is not applied.
- A tool that calls suspend() fails. Use the session's approval instead.
- Tool names must be at most 63 characters of letters, digits, _, and -, and must not start with a digit or -.
The adapter README lists every limit. See Limits for long sessions for the server limits.