Skip to content
Mayura

Workflows

Workflow composition

Run a fixed graph of tool calls in-process as an agent, or hand it to another agent as a single tool.

Sometimes you know exactly which tools to call and in what order, and you want that sequence packaged as one unit: something you can submit like an agent, or give to another agent as a single tool. mayura/workflows/ephemeral does this. It turns a workflow graph into an ordinary agent that runs inside your existing runtime, under that runtime's permissions, limits, guards and budget.

Nothing is stored. If the process stops, the run is gone, and there are no approvals, human requests or timers. When the steps must survive a restart or wait for a person, use a durable workflow instead.

A complete example

This graph trims a piece of text and then counts its characters. It needs no model and no credentials.

ts
import { createRuntime, defineTool, z } from 'mayura';
import { defineWorkflow } from 'mayura/workflows';
import { workflowAsAgent } from 'mayura/workflows/ephemeral';

const text = z.string();
const summary = z.object({ text: z.string(), characters: z.number().int() });

const normalize = defineTool({
  id: 'text.normalize', version: '1', description: 'Trim and collapse whitespace.',
  input: text, output: text, effects: 'none', capabilities: [],
  execute: value => value.trim().replace(/\s+/gu, ' '),
});
const describe = defineTool({
  id: 'text.describe', version: '1', description: 'Count the characters of a text.',
  input: text, output: summary, effects: 'none', capabilities: [],
  execute: value => ({ text: value, characters: [...value].length }),
});

const textSummary = defineWorkflow({
  id: 'text-summary',
  version: '1',
  input: text,
  output: summary,
  nodes: [
    { kind: 'tool', id: 'normalize', tool: normalize, input: { kind: 'input', path: [] } },
    { kind: 'tool', id: 'describe', tool: describe, dependsOn: ['normalize'], input: { kind: 'step', stepId: 'normalize', path: [] } },
  ],
  result: { kind: 'step', stepId: 'describe', path: [] },
});

const runtime = createRuntime({
  profile: 'ephemeral',
  permissions: { allow: ['model:mayura.workflow', 'tool:text.normalize', 'tool:text.describe'] },
  limits: { maxCostMicros: 0 },
});

try {
  const run = runtime.submit(workflowAsAgent(textSummary, { profile: 'ephemeral' }), { input: '  Hello   Mayura  ' });
  const outcome = await run.result();
  if (outcome.status === 'succeeded') console.log(outcome.output); // { text: 'Hello Mayura', characters: 12 }
} finally {
  await runtime.close();
}

defineWorkflow (from mayura/workflows) declares the graph: tool and join nodes, bindings and a result, the same building blocks described in Workflows. workflowAsAgent turns it into an agent whose "model" is a small local planner that reads the graph and asks for the next ready tools. The planner is plain code: it makes no network call and costs nothing.

Permissions

The workflow agent needs the same explicit grants as any agent:

  • model:mayura.workflow for the planner. If you set plannerId, grant model:<plannerId> instead.
  • For each tool step: tool:<id>, each capability the tool declares, and effect:<kind> for a tool with effects.

A tool the runtime does not allow fails the run, exactly as it would for a model-driven agent. See Permissions.

Use a workflow as a tool

workflowAsTool wraps the graph as a tool, so an agent (or another workflow) can call it as one step. It runs as a required child run of the caller, the same way child agents do.

ts
import { defineAgent } from 'mayura';
import { workflowAsTool } from 'mayura/workflows/ephemeral';

const summarizeText = workflowAsTool(textSummary, {
  profile: 'ephemeral',
  id: 'workflow.text-summary',
  description: 'Normalize a text and count its characters.',
  // What the child may do. It is intersected with the parent's grants: a child never gains authority.
  permissions: { allow: ['model:mayura.workflow', 'tool:text.normalize', 'tool:text.describe'] },
  limits: { maxCostMicros: 0 },
});

const editor = defineAgent({
  id: 'editor', version: '1', instructions: 'Tidy the text the user sends, then report its length.',
  input: text, output: summary, model, tools: [summarizeText],
});

The parent runtime must grant tool:workflow.text-summary and agent:delegate, plus everything in the child's list. The child's cost and calls count against the parent's limits; the parent's budget already includes them, so do not add the two together.

Options

Option Applies to Meaning
profile both Must be 'ephemeral'. It is required so the choice of an in-process run is explicit.
plannerId both Model id of the planner. Default mayura.workflow.
guards both Input and output guards for the workflow agent, as for any agent.
id, description workflowAsTool The tool's identity, shown to the calling model.
permissions workflowAsTool Grants for the child run. Required.
limits workflowAsTool Runtime limits for the child run.

How it runs

  • Each wave of ready steps uses one model call and one step from the runtime's limits, and the final answer uses one more. The default maxSteps is 16, so a long chain needs a higher limit. Defaults are never raised for you.
  • Steps that are ready together are requested together, but the runtime runs them one after another.
  • Later steps only see results that passed the tools' output guards. A failed, blocked or cancelled tool ends the run.
  • A compiled workflow holds no per-run state, so you can submit it many times at once.

Always check outcome.status before reading the output. A run that failed part-way may still have caused effects in earlier steps; runtime.inspect(run) lists what executed. Cancelling does not undo an effect.

Good to know

  • A graph with an approval: true step is refused. Approvals need a durable workflow.
  • The graph has at most 128 nodes and no cycles. For data-dependent branching, let an agent decide.
  • The same defineWorkflow graph can also run durably through the earlier runtimes in mayura/workflows, but new durable work should use defineWorkflowLifecycle; see Durable workflows.