Skip to main content
BAML and Smithers solve different layers of the same workflow.
  • BAML owns prompt authoring, model calls, structured parsing, and the generated baml_client.
  • Smithers owns orchestration, durable execution, retries, approvals, caching, and persistence.
This integration lets you import a .baml file directly into a Smithers JSX workflow and render each BAML function as a task component.

What this integration does

For each BAML function in an imported .baml file, Smithers exposes:
  • a React component named after the function
  • a Zod schema export named <FunctionName>Output
A .baml import is already a task. Do not wrap a generated BAML export in <Task agent={...}>, and do not treat it as prompt content for another <Task>. The generated component renders a Smithers compute task internally.

Prerequisites

Start with the JSX Installation guide, then add BAML support. Install BAML:
bun add @boundaryml/baml
If you do not already have a baml_src/ directory, initialize one:
bun baml-cli init

1. Configure BAML generators

Create or update baml_src/generators.baml:
generator typescript {
  output_type "typescript"
  output_dir "../"
  version "<match your installed @boundaryml/baml version>"
  module_format "esm"
}

generator smithers {
  output_type "typescript/smithers"
  output_dir "../"
  version "<match your installed @boundaryml/baml version>"
  module_format "esm"
}
Use the normal TypeScript generator for baml_client, and the Smithers generator for task wrappers and output schemas.

2. Register the Bun preload plugin

Create preload.ts:
import { bamlPlugin, mdxPlugin } from "smithers-orchestrator";

bamlPlugin();
mdxPlugin(); // optional; only needed if you also import .mdx prompts
Then register it in bunfig.toml:
preload = ["./preload.ts"]
Keep baml-cli generate in your build and CI flow so generated artifacts stay current.

3. Write a BAML function

class Story {
  title string
  content string
}

function WriteMeAStory(input: string) -> Story {
  client "openai/gpt-5"

  prompt #"
    Write a short story about the topic below.

    {{ ctx.output_format() }}

    {{ _.role("user") }}
    Topic: {{ input }}
  "#
}

4. Import the .baml file into a workflow

/** @jsxImportSource smithers-orchestrator */
import { createSmithers, Task, Workflow } from "smithers-orchestrator";
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import * as Story from "./baml_src/story.baml";

const critic = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  instructions: "You are a concise editor.",
});

const { smithers, outputs } = createSmithers({
  writeStory: Story.WriteMeAStoryOutput,
  review: z.object({
    verdict: z.enum(["pass", "revise"]),
    notes: z.string(),
  }),
});

export default smithers((ctx) => {
  const story = ctx.outputMaybe(outputs.writeStory, { nodeId: "write-story" });

  return (
    <Workflow name="story-flow">
      <Story.WriteMeAStory
        id="write-story"
        args={{ input: ctx.input.topic }}
        timeoutMs={30_000}
        retries={1}
      />

      {story ? (
        <Task id="review-story" output={outputs.review} agent={critic}>
          {`Review this story.

Title: ${story.title}

Content:
${story.content}`}
        </Task>
      ) : null}
    </Workflow>
  );
});

Generated API

For a BAML function named WriteMeAStory, the generated .baml import exposes:
  • WriteMeAStory — a React component that renders a Smithers compute task
  • WriteMeAStoryOutput — the generated Zod schema for the persisted output
The generated component accepts:
  • id — the Smithers node id
  • args — the BAML function arguments, grouped under one prop to avoid collisions with Smithers task props
  • bamlOptions — forwarded to the generated baml_client call
  • normal Smithers task controls such as dependsOn, needs, skipIf, needsApproval, timeoutMs, retries, retryPolicy, continueOnFail, cache, label, and meta
Example:
<Story.WriteMeAStory
  id="write-story"
  args={{ input: "A detective story set on Mars" }}
  timeoutMs={20_000}
  retries={2}
/>

Output registration

Register the generated output schema with createSmithers(...) like any other Zod schema:
const { smithers, outputs } = createSmithers({
  writeStory: Story.WriteMeAStoryOutput,
});
The key you choose in createSmithers(...) is your Smithers storage name. It does not need to match the BAML function name. Downstream tasks read it through outputs like any other schema-driven output:
const story = ctx.outputMaybe(outputs.writeStory, { nodeId: "write-story" });

Object returns vs scalar returns

If a BAML function returns an object-like type, that shape is persisted directly. If a BAML function returns a scalar, enum, list, map, or union, the generated Smithers schema boxes the value under result so the output stays object-shaped for persistence. Example:
function Classify(input: string) -> "positive" | "negative" {
  client "openai/gpt-5"
  prompt #"Classify the sentiment.

{{ ctx.output_format() }}

{{ _.role("user") }}
{{ input }}"#
}
The generated Smithers output is conceptually:
const ClassifyOutput = z.object({
  result: z.enum(["positive", "negative"]),
});
So downstream code reads ctx.outputMaybe(outputs.classify, { nodeId: "classify" })?.result.

How validation works

BAML handles:
  • prompt rendering
  • model execution
  • structured parsing
  • repair / retry logic inside the BAML call path
Smithers then treats the generated component as a compute task:
  1. the generated wrapper calls baml_client
  2. Smithers validates the final parsed value against the generated Zod schema
  3. Smithers persists the validated result
  4. the workflow re-renders and downstream nodes can read the output
That means BAML-generated tasks compose cleanly with normal Smithers <Task> nodes, approvals, loops, and caching.

When to use BAML vs MDX prompts

Use BAML when:
  • the prompt and output schema belong together
  • you want BAML’s structured-output ergonomics
  • you want to reuse the same BAML function outside Smithers
Use MDX prompts when:
  • the prompt is mostly prose or documentation
  • you want JSX/MDX composition for prompt text
  • you want to keep using Smithers agent mode directly
You can mix both styles in the same workflow.

Current limitations

This integration is intentionally narrow in v1. Supported well:
  • static BAML return types
  • normal Smithers task controls
  • mixing generated BAML tasks with normal Smithers JSX tasks
Not supported in v1:
  • return types whose shape changes at runtime via @@dynamic / TypeBuilder
  • streaming partial outputs into Smithers
  • multimodal return values as Smithers task outputs
If you need dynamic output schemas, keep that function outside the generated Smithers-task path for now.

Troubleshooting

”Cannot import .baml”

Make sure bamlPlugin() is registered in a Bun preload file and that bunfig.toml uses top-level preload.

”Generated files are missing”

Run:
bun baml-cli generate
and make sure your BAML generators are configured.

”My downstream task cannot read the output”

Register the generated schema in createSmithers(...) and read it through the returned outputs object.

”My BAML task output shape does not match”

Check whether the function returns a scalar or dynamic type. Scalars are boxed under result, and dynamic output shapes are not supported in v1.