Skip to content
Foundry
Esc
↑↓navigate↵open⌘Jpreview
On this page

Steps and workflow composition

Connect typed results into a tree that executes in dependency order.

A step owns one piece of work. A workflow gives a connected tree of steps a name and launch input. A parent receives its children’s results, so children run before the parent.

Marbles builds this tree on Workflows. The same definition can occupy a position in more than one tree. Its position determines which results it receives in this workflow.

Inputs and results

A step accepts input and returns a result. .input(schema) validates the input and supplies its TypeScript type; .output(schema) validates the result. Return plain JSON data so Marbles can record it and hand it to another step.

A parent declares the child results it consumes in its input schema. In the example below, count returns a number and summarize receives that number as input.count.

Hand a result to the next step

Install zod@4.4.3 in your project. Save this complete module as .foundry/marbles/summary.ts:

import { step, workflow } from "@foundry.sh/marbles";
import { z } from "zod";

const count = step("count-matching")
  .input(z.object({ extension: z.string() }))
  .output(z.number())
  .do(async ({ input, workspaces }) => {
    const files = await workspaces.current.files();
    return files.filter((file) => file.path.endsWith(input.extension)).length;
  });

const summarize = step("summarize-count")
  .input(z.object({ count: z.number(), extension: z.string() }))
  .output(z.string())
  .do(({ input }) => `${input.count} files end in ${input.extension}`);

workflow("file-summary")
  .input(z.object({ extension: z.string() }))
  .do(({ input }) =>
    summarize({ count: count({}, { extension: input.extension }) }, input)
  );

Run marbles roll file-summary --input '{"extension":".ts"}'. The workflow returns a sentence containing the count.

summarize(children, literalInput) takes children first and literal input second. count supplies input.count; the literal supplies input.extension. A key cannot appear in both. The parent’s input schema declares both fields.

Run independent work together

This separate module counts two kinds of file concurrently:

import { step, workflow } from "@foundry.sh/marbles";
import { z } from "zod";

const count = step("count-kind")
  .input(z.object({ extension: z.string() }))
  .output(z.number())
  .do(
    async ({ input, workspaces }) =>
      (await workspaces.current.files()).filter((file) =>
        file.path.endsWith(input.extension)
      ).length
  );

const combine = step("combine-counts")
  .input(z.object({ typescript: z.number(), markdown: z.number() }))
  .do(({ input }) => input);

workflow(
  "project-counts",
  combine.parallel({
    typescript: count({}, { extension: ".ts" }),
    markdown: count({}, { extension: ".md" }),
  })
);

marbles roll project-counts returns both counts. Calling combine(...) or combine.series(...) runs children in key order; .parallel(...) runs them together. Data flows from children to their parent, never between siblings. If one parallel child fails, siblings are cancelled and awaited.

Choose what belongs in a step

Make an operation a child step when its result, failure, or progress should be independently visible. Keep ordinary calculations inside a body. A workflow can be a launch root, but cannot be nested as a child in another tree; reuse its steps instead.

.run(input) executes a definition directly, including from inside a body. Its nested execution does not appear as an independent dashboard run. Lock a child into the tree when you want that visibility.

There are no race, branching, or loop combinators. You can choose a tree in ordinary TypeScript during setup. Keep the actual commands, file access, and agent turns inside steps. The builder reference lists the exact composition forms.

This choice also determines what Marbles can retry or keep while waiting. Durability explains those rules after the resource chapters. Next: Contexts and bindings explains what setup and step callbacks receive when this tree executes.

Was this page helpful?