Share what a Mastra agent produced
A Mastra agent finishes its run and returns text. A workflow finishes and
returns whatever the last step's output schema declared. Both end up in a
server response, and neither is something you can forward to a colleague.
If the agent built an HTML report, the best case is that the report is now a very large string in a JSON body.
Option one — a tool
Mastra's tools are typed, so the contract is visible to you and to the model:
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const publishReport = createTool({
id: "publish-report",
description: "Publish an HTML report and return its shareable URL.",
inputSchema: z.object({ title: z.string(), html: z.string() }),
outputSchema: z.object({ url: z.string() }),
execute: async ({ context }) => {
const res = await fetch("https://commareports.com/api/v1/reports", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COMMA_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: context.title, html: context.html }),
});
if (!res.ok) throw new Error(`publish failed: ${res.status}`);
const { url } = await res.json();
return { url };
},
});
Then say so in the agent's instructions — a tool the agent is not told to reach for is a tool it will use sometimes:
instructions: `Do the analysis. Render it as a self-contained HTML report,
publish it with publish-report, and reply with only the URL.`,
Option two — MCPClient
import { MCPClient } from "@mastra/mcp";
const mcp = new MCPClient({
servers: {
comma: {
url: new URL("https://commareports.com/api/mcp"),
requestInit: {
headers: { Authorization: `Bearer ${process.env.COMMA_API_TOKEN}` },
},
},
},
});
const agent = new Agent({ name: "analyst", tools: await mcp.getTools() });
Publishing, updating and reading comments arrive together, and the same configuration works from your editor — see MCP setup.
Workflows: publish as the last step
const publishStep = createStep({
id: "publish",
inputSchema: z.object({ title: z.string(), html: z.string() }),
outputSchema: z.object({ url: z.string() }),
execute: async ({ inputData }) => ({ url: await publish(inputData) }),
});
Put it before any suspend. A workflow that pauses for human approval and
hands over a raw payload is asking someone to review a document they cannot
read; a workflow that pauses after publishing hands over a page with comment
threads, and the resume value can be what the reviewer wrote — see
commenting on HTML.
Worth knowing
- Deployed means no filesystem. On Mastra Cloud or any container, writing a file produces nothing a reader can reach — the same problem CI jobs have.
- Keep the URL in workflow state, not the HTML. Large strings get carried through every subsequent step.
reports:writeis enough to publish — see scoped tokens for AI agents.- 5 MB per report body.
Try it
Free — unlimited reports, commenters and revisions.