Array as Input
This example demonstrates how to process an array input in a workflow. Mastra provides a .foreach()
helper function that executes a step for each item in the array.
Setup
npm install @ai-sdk/openai @mastra/core simple-git
Define Docs Generator Agent
Define a docs generator agent that leverages an LLM call to generate a documentation given a code file or a summary of a code file.
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
// Create a documentation generator agent for code analysis
const docGeneratorAgent = new Agent({
name: "doc_generator_agent",
instructions: `You are a technical documentation expert. You will analyze the provided code files and generate a comprehensive documentation summary.
For each file:
1. Identify the main purpose and functionality
2. Document key components, classes, functions, and interfaces
3. Note important dependencies and relationships between components
4. Highlight any notable patterns or architectural decisions
5. Include relevant code examples where helpful
Format the documentation in a clear, organized manner using markdown with:
- File overviews
- Component breakdowns
- Code examples
- Cross-references between related components
Focus on making the documentation clear and useful for developers who need to understand and work with this codebase.`,
model: openai("gpt-4o"),
});
export { docGeneratorAgent };
Define File Summary Workflow
Define the file summary workflow with 2 steps: one to fetch the code of a particular file and another to generate a readme for that particular code file.
import { createWorkflow, createStep } from "@mastra/core/workflows/vNext";
import { docGeneratorAgent } from "../agents/docs-generator-agent";
import { z } from "zod";
import fs from "fs";
// Step 1: Read the code content from a file
const scrapeCodeStep = createStep({
id: "scrape_code",
description: "Scrape the code from a single file",
inputSchema: z.string(),
outputSchema: z.object({
path: z.string(),
content: z.string(),
}),
execute: async ({ inputData }) => {
const filePath = inputData;
const content = fs.readFileSync(filePath, "utf-8");
return {
path: filePath,
content,
};
},
});
// Step 2: Generate documentation for a single file
const generateDocForFileStep = createStep({
id: "generateDocForFile",
inputSchema: z.object({
path: z.string(),
content: z.string(),
}),
outputSchema: z.object({
path: z.string(),
documentation: z.string(),
}),
execute: async ({ inputData }) => {
const docs = await docGeneratorAgent.generate(
`Generate documentation for the following code: ${inputData.content}`,
);
return {
path: inputData.path,
documentation: docs.text.toString(),
};
},
});
const generateSummaryWorkflow = createWorkflow({
id: "generate-summary",
inputSchema: z.string(),
outputSchema: z.object({
path: z.string(),
documentation: z.string(),
}),
steps: [scrapeCodeStep, generateDocForFileStep],
})
.then(scrapeCodeStep)
.then(generateDocForFileStep)
.commit();
export { generateSummaryWorkflow };
Define Readme Generator Workflow
Define a readme generator workflow with 4 steps: one to clone the github repository, one to suspend the workflow and get user input on what all folders to consider while generating a readme, one to generate a summary of all the files inside the folder, and another to collate all the documentation generated for each file into a single readme.
import { createWorkflow, createStep } from "@mastra/core/workflows/vNext";
import { docGeneratorAgent } from "../agents/docs-generator-agent";
import { generateSummaryWorkflow } from "./file-summary-workflow";
import { z } from "zod";
import simpleGit from "simple-git";
import fs from "fs";
import path from "path";
// Step 1: Clone a GitHub repository locally
const cloneRepositoryStep = createStep({
id: "clone_repository",
description: "Clone the repository from the given URL",
inputSchema: z.object({
repoUrl: z.string(),
}),
outputSchema: z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
repoUrl: z.string(),
}),
}),
execute: async ({
inputData,
mastra,
getStepResult,
getInitData,
runtimeContext,
}) => {
const git = simpleGit();
// Skip cloning if repo already exists
if (fs.existsSync("./temp")) {
return {
success: true,
message: "Repository already exists",
data: {
repoUrl: inputData.repoUrl,
},
};
}
try {
// Clone the repository to the ./temp directory
await git.clone(inputData.repoUrl, "./temp");
return {
success: true,
message: "Repository cloned successfully",
data: {
repoUrl: inputData.repoUrl,
},
};
} catch (error) {
throw new Error(`Failed to clone repository: ${error}`);
}
},
});
// Step 2: Get user input on which folders to analyze
const selectFolderStep = createStep({
id: "select_folder",
description: "Select the folder(s) to generate the docs",
inputSchema: z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
repoUrl: z.string(),
}),
}),
outputSchema: z.array(z.string()),
suspendSchema: z.object({
folders: z.array(z.string()),
message: z.string(),
}),
resumeSchema: z.object({
selection: z.array(z.string()),
}),
execute: async ({ resumeData, suspend }) => {
const tempPath = "./temp";
const folders = fs
.readdirSync(tempPath)
.filter((item) => fs.statSync(path.join(tempPath, item)).isDirectory());
if (!resumeData?.selection) {
await suspend({
folders,
message: "Please select folders to generate documentation for:",
});
return [];
}
// Gather all file paths from selected folders
const filePaths: string[] = [];
// Helper function to recursively read files from directories
const readFilesRecursively = (dir: string) => {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
readFilesRecursively(fullPath);
} else if (stat.isFile()) {
filePaths.push(fullPath.replace(tempPath + "/", ""));
}
}
};
for (const folder of resumeData.selection) {
readFilesRecursively(path.join(tempPath, folder));
}
return filePaths;
},
});
// Step 4: Combine all documentation into a single README
const collateDocumentationStep = createStep({
id: "collate_documentation",
inputSchema: z.array(
z.object({
path: z.string(),
documentation: z.string(),
}),
),
outputSchema: z.string(),
execute: async ({ inputData }) => {
const readme = await docGeneratorAgent.generate(
`Generate a README.md file for the following documentation: ${inputData.map((doc) => doc.documentation).join("\n")}`,
);
return readme.text.toString();
},
});
const readmeGeneratorWorkflow = createWorkflow({
id: "readme-generator",
inputSchema: z.object({
repoUrl: z.string(),
}),
outputSchema: z.object({
success: z.boolean(),
message: z.string(),
data: z.object({
repoUrl: z.string(),
}),
}),
steps: [cloneRepositoryStep, selectFolderStep, generateSummaryWorkflow, collateDocumentationStep],
})
.then(cloneRepositoryStep)
.then(selectFolderStep)
.foreach(generateSummaryWorkflow)
.then(collateDocumentationStep)
.commit();
export { readmeGeneratorWorkflow };
Register Agent and Workflow instances with Mastra class
Register the agents and workflow with the mastra instance. This is critical for enabling access to the agents within the workflow.
import { createLogger, Mastra } from "@mastra/core";
import { docGeneratorAgent } from "./agents/docs-generator-agent";
import { readmeGeneratorWorkflow } from "./workflows/readme-generator-workflow";
import { generateSummaryWorkflow } from "./workflows/file-summary-workflow";
// Create a new Mastra instance and register components
const mastra = new Mastra({
agents: {
docGeneratorAgent,
},
vnext_workflows: {
readmeGeneratorWorkflow,
generateSummaryWorkflow,
},
logger: createLogger({
name: 'Mastra',
level: 'info',
}),
});
export { mastra };
Execute the Readme Generator Workflow
Here, we’ll get the reamde generator workflow from the mastra instance, then create a run and execute the created run with the required inputData.
import { promptUserForFolders } from "./utils";
import { mastra } from "./";
// GitHub repository to generate documentation for
const ghRepoUrl = "https://github.com/mastra-ai/mastra";
const run = mastra.vnext_getWorkflow("readmeGeneratorWorkflow").createRun();
// Start the workflow with the repository URL as input
const res = await run.start({ inputData: { repoUrl: ghRepoUrl } });
const { status, steps } = res;
// Handle suspended workflow (waiting for user input)
if (status === "suspended") {
// Get the suspended step data
const suspendedStep = steps["select_folder"];
let folderList: string[] = [];
// Extract the folder list from step data
if (
suspendedStep.status === "suspended" &&
"folders" in suspendedStep.payload
) {
folderList = suspendedStep.payload.folders as string[];
} else if (suspendedStep.status === "success" && suspendedStep.output) {
folderList = suspendedStep.output;
}
if (!folderList.length) {
console.log("No folders available for selection.");
process.exit(1);
}
// Prompt user to select folders
const folders = await promptUserForFolders(folderList);
// Resume the workflow with user selections
const resumedResult = await run.resume({
resumeData: { selection: folders },
step: "select_folder",
});
// Print resumed result
if (resumedResult.status === "success") {
console.log(resumedResult.result);
} else {
console.log(resumedResult);
}
process.exit(1);
}
// Handle completed workflow
if (res.status === "success") {
console.log(res.result ?? res);
} else {
console.log(res);
}