Get started
Quickstart
Create a Mayura project in one command, or add a first agent to your own project and run it offline or on a real model.
There are two ways to start: let the CLI create a complete project for you, or add Mayura to a project you already have. Both take a few minutes.
Option 1: create a project
npx mayura initThe wizard asks three things:
- A starter or a template. Starters are complete projects with tests: four are servers with a worker, a UI and
deployment files, and one (
cli-agent) is an assistant you run in your terminal. Templates are single files that show one feature. - A model provider. Offline (no key needed; rule-based stand-in models), OpenAI, Anthropic, an OpenAI-compatible provider such as Groq, Gemini, Mistral, DeepSeek, xAI, OpenRouter, Together or Fireworks, Azure OpenAI, or any other compatible endpoint.
- Your API key, prices and spending caps, for a real provider. The key is typed masked and written only to the
new project's
.env, which git ignores.
It shows the plan, writes the files when you confirm, and prints the next steps. For a starter those are:
cd my-agent
npm install
npm run devnpm run dev runs mayura dev: it builds the project, starts it, and rebuilds and restarts whenever you save. See
mayura init for every starter and template.
Option 2: add Mayura to your project
Install Mayura. It includes z (Zod) for schemas, so there is nothing else to add:
npm install mayuraMayura is an ES module package, so your project needs "type": "module" in package.json (or .mts files).
A first agent, offline
This agent uses a tool to add two numbers. It runs on a scripted model that replays fixed responses, so it needs no API key and no network. It's the same way you test agents.
import { createRuntime, defineAgent, defineTool, z } from 'mayura';
import { scriptedModel } from 'mayura/testing';
const add = defineTool({
id: 'math.add', version: '1', description: 'Add two numbers.',
input: z.object({ left: z.number(), right: z.number() }),
output: z.object({ sum: z.number() }),
effects: 'none', capabilities: [],
execute: ({ left, right }) => ({ sum: left + right }),
});
const agent = defineAgent({
id: 'calculator', version: '1', instructions: 'Use the addition tool to answer.',
input: z.object({ request: z.string() }), output: z.object({ answer: z.number() }), tools: [add],
model: scriptedModel([
{ type: 'tool_calls', calls: [{ id: 'add-1', toolId: 'math.add', input: { left: 2, right: 3 } }], usage: { costMicros: 0 } },
{ type: 'final', output: { answer: 5 }, usage: { costMicros: 0 } },
]),
});
const runtime = createRuntime({ profile: 'ephemeral', permissions: { allow: ['model:scripted', 'tool:math.add'] } });
try {
const result = await runtime.submit(agent, { input: { request: 'Add 2 and 3.' } }).result();
console.log(result); // { status: 'succeeded', output: { answer: 5 } }
} finally {
await runtime.close();
}Save it as agent.ts and run it. Node.js 24 runs TypeScript directly; on Node.js 22, use npx tsx agent.ts or
compile with tsc.
node agent.tsFour things happened:
defineTooldeclared a tool: schemas for its input and output, what kind of effect it has (none,read,writeorhost), and the function that does the work. Mayura validates the input before calling it and the output after.defineAgentdeclared an agent: instructions, schemas, tools and a model.createRuntimecreated the runtime that runs agents, with an explicit allow-list. The agent can usemath.addonly becausetool:math.addis granted; remove it and the run is blocked.runtime.submitstarted a run andresult()waited for its outcome. Always checkresult.statusbefore readingresult.output: a run can also endfailed,blocked,cancelledoroutcome_unknown. See Outcomes.
Use a real model
Swap the scripted model for a provider. A real model needs your model's prices and cost caps: a run is allowed to
spend nothing until you set limits.maxCostMicros. Costs are in micros, millionths of a dollar.
import { createRuntime, defineAgent, defineTool, z } from 'mayura';
import { anthropicMessages } from 'mayura/provider-anthropic';
const add = defineTool({
id: 'math.add', version: '1', description: 'Add two numbers.',
input: z.object({ left: z.number(), right: z.number() }),
output: z.object({ sum: z.number() }),
effects: 'none', capabilities: [],
execute: ({ left, right }) => ({ sum: left + right }),
});
const agent = defineAgent({
id: 'calculator', version: '1', instructions: 'Use the addition tool to answer.',
input: z.object({ request: z.string() }), output: z.object({ answer: z.number() }), tools: [add],
model: anthropicMessages({
apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-sonnet-5',
pricing: { inputMicrosPerMillionTokens: 3_000_000, outputMicrosPerMillionTokens: 15_000_000 }, // check your model's prices
maxCostMicros: 50_000, // at most 5 cents per model call
}),
});
const runtime = createRuntime({
profile: 'ephemeral',
permissions: { allow: ['model:anthropic.messages', 'tool:math.add'] },
limits: { maxCostMicros: 200_000 }, // at most 20 cents per run
});The rest of the program is unchanged. The provider needs the tool's input and the agent's output as JSON Schema;
Mayura generates both from your Zod schemas (Zod 4.2 or later). Providers accept only strict schemas, where every
field is present, so use .nullable() rather than .optional() for a field the model may leave empty.
defineAgent checks this and tells you exactly which field to change.
OpenAI (openAIResponses) and OpenAI-compatible providers
(openAICompatibleChat) work the same way; each is granted as model: followed by its adapter id. See
Model providers. Mayura never reads API keys from the environment by itself: you pass
them in, so where they come from is up to you.
Watch a run
A run reports what it's doing as events. observe() returns them as an async iterable, and runtime.inspect shows
what the run has spent:
const run = runtime.submit(agent, { input: { request: 'Add 2 and 3.' } });
for await (const event of run.observe()) {
if (event.type === 'tool.started') console.log('using', event.metadata.toolId);
}
const result = await run.result();
console.log(runtime.inspect(run).budget.spentMicros);Events carry metadata such as ids, steps, statuses and costs, not your prompts or tool inputs and outputs. The one
exception is output.delta, which carries streamed answer text (below).
Stream the answer
To show an answer while it's being written, give the agent a stream policy naming the output field to stream, and
print the output.delta events:
import { defineAgent } from 'mayura';
const assistant = defineAgent({
id: 'assistant', version: '1', instructions: 'Answer helpfully.',
input, output, tools: [], model,
stream: { field: ['reply'], guards: [] }, // stream output.reply
});
const run = runtime.submit(assistant, { input: { question: 'What is Mayura?' } });
for await (const event of run.observe()) {
if (event.type === 'output.delta') process.stdout.write(String(event.metadata.text));
}The final result() is still validated against the output schema; the streamed text is a preview. See
Streaming.
Next steps
- Agents, tools and the runtime in depth.
- Durable workflows: steps, approvals and timers that survive restarts.
- Serve agents over HTTP and call them from a browser or React.
- Chat with an agent in the terminal.
- Deploy a Mayura application.