Skip to main content
MDX lets you write prompts as files with JSX interpolation. This separates prompt engineering from orchestration logic, making prompts easier to read, edit, and maintain. Smithers uses MDX in two ways:
  1. Per-step prompts.mdx files with {props.*} interpolation, used as children of <Task>.
  2. System prompt composition — A master .mdx template that assembles a system prompt from multiple markdown docs using JSX component injection.

Setup

Enable MDX imports in Bun with two files:
// preload.ts
import { mdxPlugin } from "smithers-orchestrator/mdx-plugin";
mdxPlugin();
# bunfig.toml
preload = ["./preload.ts"]

Per-Step Prompts

Each step gets an .mdx file that serves as its prompt template. Props are interpolated at render time:
{/* components/Review.mdx */}
CODE REVIEW -- Ticket: {props.ticketId} -- {props.ticketTitle}

Reviewer: {props.reviewer}

TICKET DESCRIPTION:
{props.ticketDescription}

ACCEPTANCE CRITERIA:
- {props.acceptanceCriteria}

FILES CHANGED:
Created: {JSON.stringify(props.filesCreated)}
Modified: {JSON.stringify(props.filesModified)}

{props.failingSummary
  ? `VALIDATION FAILURES:\n${props.failingSummary}`
  : "All tests passing."}

**REQUIRED OUTPUT** -- JSON matching this schema:
{props.schema}
Use it in your component:
// components/Review.tsx
import { Task } from "../smithers";
import { claude } from "../agents";
import ReviewPrompt from "./Review.mdx";

export function Review({ ticket }) {
  return (
    <Task id={`${ticket.id}:review`} output={tables.review} agent={claude}>
      <ReviewPrompt
        ticketId={ticket.id}
        ticketTitle={ticket.title}
        ticketDescription={ticket.description}
        acceptanceCriteria={ticket.acceptanceCriteria?.join("\n- ") ?? ""}
        filesCreated={["src/auth.ts"]}
        filesModified={["src/index.ts"]}
        failingSummary={null}
        reviewer="claude"
      />
    </Task>
  );
}

Auto-injected {props.schema}

When a <Task> has an outputSchema (either explicit or auto-resolved from createSmithers()), Smithers auto-injects a schema prop into the MDX component. This prop contains a human-readable JSON example generated from the Zod schema, so agents know exactly what structure to produce. You do not need to pass schema manually — just use {props.schema} in your MDX and it will be filled in automatically.

System Prompt Composition

For large workflows, the system prompt needs to include project documentation, coding conventions, architecture notes, and more. MDX lets you compose this from standalone markdown files.

Step 1: Write standalone docs

Keep domain knowledge in .md files that are reusable across workflows:
prompts/
  system-prompt.mdx     # Master template
  architecture.md       # Architecture docs
  coding-standards.md   # Coding conventions
  git-rules.md          # Git commit conventions
  always-green.md       # "Keep tests passing" rules

Step 2: Create component functions

In system-prompt.ts, read each doc file and wrap it in a component function:
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { renderMdx } from "smithers-orchestrator";
import SystemPromptMdx from "./prompts/system-prompt.mdx";

const ROOT = resolve(new URL("../..", import.meta.url).pathname);
const PROMPTS = resolve(new URL("./prompts", import.meta.url).pathname);

function readDoc(path: string): string {
  try { return readFileSync(resolve(ROOT, path), "utf8"); }
  catch { return `[Could not read ${path}]`; }
}

function readPrompt(filename: string): string {
  try { return readFileSync(resolve(PROMPTS, filename), "utf8"); }
  catch { return `[Could not read prompt: ${filename}]`; }
}

const ClaudeMd = () => readDoc("CLAUDE.md");
const Architecture = () => readPrompt("architecture.md");
const CodingStandards = () => readPrompt("coding-standards.md");
const GitRules = () => readPrompt("git-rules.md");
const AlwaysGreen = () => readPrompt("always-green.md");

export const SYSTEM_PROMPT = renderMdx(SystemPromptMdx, {
  components: {
    ClaudeMd,
    Architecture,
    CodingStandards,
    GitRules,
    AlwaysGreen,
  },
});

Step 3: Write the master template

The system-prompt.mdx uses those components as JSX tags:
# My Project

You are building [project description].

## Project Conventions

<ClaudeMd />

## Architecture

<Architecture />

## Coding Standards

<CodingStandards />

## Git Rules

<GitRules />

## Quality Rules

<AlwaysGreen />

## JSON Output Requirement

MUST end response with JSON object in code fence. Format specified in task prompt.

Benefits

  • Reusable docs — Each .md file is standalone and can be used across multiple workflows or shared with human developers.
  • Composable — Add or remove context sections without editing prompt text. Just add/remove a component tag.
  • Shared across agents — All agents in the workflow get the same system prompt, ensuring consistent behavior.
  • Version-controlled — Prompts live in the repo and are tracked by git. Changes are visible in diffs.

Conditional Sections in Prompts

Use JavaScript ternary expressions for conditional prompt content:
{props.previousAttempt
  ? `PREVIOUS ATTEMPT FAILED:
What was done: ${props.previousAttempt.whatWasDone}
Test output: ${props.previousAttempt.testOutput}
Fix the issues above before proceeding.`
  : "This is the first attempt. Start fresh."}

{props.reviewFixes
  ? `REVIEW FIXES NEEDED:\n${props.reviewFixes}`
  : ""}
This keeps prompts concise on first iterations and adds context only when previous attempts exist.

Array Rendering

Render arrays using .map() and .join():
FILES TO MODIFY:
{props.filesToModify.map(f => `- ${f}`).join("\n")}

ACCEPTANCE CRITERIA:
{props.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}

REVIEW ISSUES:
{JSON.stringify(props.issues, null, 2)}

Next Steps