DEV Community

Cover image for Introducing: AML - Agent Markup Language
Jonathan Tavares
Jonathan Tavares

Posted on

Introducing: AML - Agent Markup Language

Our first agent workflows were easy to start. Then we added another agent, a tool, a follow-up turn, a sandbox, and somewhere to keep files between runs.

The code still worked, but understanding the workflow meant jumping between provider calls, prompt builders, schema parsing, and cleanup code. We wanted to open one file and see what ran, what each agent could use, and where its result went.

So we built Agent Markup Language.

What goes in the tree

AML lets you write agent workflows as TypeScript and JSX.

Agents, their capabilities, and resource boundaries live in one executable tree. The runtime evaluates that tree, manages provider and resource lifecycles, and returns the final agent's output.

<Sandbox access="read-only" provider={Docker} root="repository">
  <Agent provider={Codex}>Inspect src/index.ts.</Agent>
</Sandbox>
Enter fullscreen mode Exit fullscreen mode

That nesting has runtime meaning. Put an <Agent> inside a <Sandbox> and, bam, its model-controlled filesystem and command execution are scoped to that sandbox. Same with skills, you can put a <Skill> or <Tool> inside an <Agent> and AML runtime makes sure everything resolves before prompting the agent.

AML currently ships with OpenCode and Codex adapters, a Docker sandbox, a local workspace provider, (and deterministic providers for tests). It is early, and the API is still taking shape.

We were tired of rebuilding the glue

Provider SDKs handle model sessions well. They know how to authenticate, send turns, call native tools, and keep provider-specific state.

A useful workflow usually has more going on around that session. It may run two specialists in parallel, validate their output, and pass both results to a coordinator, mix in results from external APIs or a database.

We kept writing that orchestration by hand.

async function Review({ source }: { source: string }) {
  const findings = await evaluate(
    <Agent provider={OpenCode}>
      Find correctness issues in this source:
      {source}
    </Agent>
  )

  return (
    <Agent provider={Codex}>
      Check these findings against the source:
      {source}
      {findings}
    </Agent>
  )
}
Enter fullscreen mode Exit fullscreen mode

JSX already knows how to describe a tree

Agent workflows naturally form trees: coordinators depend on specialists, tools belong to particular agents, and sandboxes wrap the work that needs them. XML-style markup makes those relationships visible.

JSX adds the TypeScript tooling we already use: autocomplete, type checking, refactoring, and syntax highlighting. React developers already know components, props, children, and ordinary JavaScript control flow.

The mental flip is execution order. AML discovers components from the root, but nested agent sessions execute from the leaves back up. A child finishes, its output joins the parent's request, and then the parent starts.

Results move up the tree

Here is a small parallel review:

import { readFile } from "node:fs/promises"

import { Agent, AmlRuntime, evaluate, opencodeAgent } from "@aml-jsx/sdk"

const OpenCode = opencodeAgent({})

async function Review() {
  const source = await readFile("src/index.ts", "utf8")

  const [correctness, maintainability] = await Promise.all([
    evaluate(
      <Agent provider={OpenCode}>
        Review this source for correctness problems:
        {source}
      </Agent>
    ),
    evaluate(
      <Agent provider={OpenCode}>
        Review this source for maintainability problems:
        {source}
      </Agent>
    ),
  ])

  return (
    <Agent provider={OpenCode}>
      Combine these reviews without inventing findings:
      {correctness}
      {maintainability}
    </Agent>
  )
}

const runtime = new AmlRuntime()
console.log(await runtime.evaluate(<Review />))
Enter fullscreen mode Exit fullscreen mode

Promise.all() starts the two reviewers together. Their outputs come back as ordinary values and land in the final agent's prompt. The coordinator runs after both reviewers finish.

Concurrency is ordinary JavaScript. Providers are passed into the tree, so the same workflow can run with Codex, OpenCode, or a deterministic test provider.

One agent can brief the next

An agent can generate part of another agent's system prompt:

import { Agent, opencodeAgent, System } from "@aml-jsx/sdk"

const OpenCode = opencodeAgent({})

function PullRequestReview({ guidelines, patch }: { guidelines: string; patch: string }) {
  return (
    <Agent provider={OpenCode} system="Coordinate the review.">
      <System>
        <Agent provider={OpenCode}>
          Turn these repository guidelines into one review rule:
          {guidelines}
        </Agent>
      </System>
      Review this pull request patch:
      {patch}
    </Agent>
  )
}
Enter fullscreen mode Exit fullscreen mode

The specialist runs first. Its final text becomes system content for the coordinator, which starts only after its full request has been assembled.

Tool access follows the tree too. If the specialist receives a <Tool> or <Mcp> grant, that grant stays with the specialist. A sibling or parent does not quietly inherit it.

Run AML yourself

With Node 26 or newer, you can run the deterministic review example without model credentials:

git clone https://github.com/we-are-singular/aml.git
cd aml
npm install
npm run example -- review
Enter fullscreen mode Exit fullscreen mode

To use AML in your own project, install the SDK:

npm install @aml-jsx/sdk
Enter fullscreen mode Exit fullscreen mode

Then point TypeScript at AML's JSX runtime:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@aml-jsx/sdk"
  }
}
Enter fullscreen mode Exit fullscreen mode

The project site has an interactive walkthrough, and the repository has runnable examples for parallel agents, structured output, follow-up turns, loops, tools, MCP, sandboxes, and workspaces.

Try one of the examples, then tell us: which part of the API made you stop and reach for the docs? Open an issue and let us know what you would simplify. We will use that feedback as we decide what to change next.

And if AML looks useful, star us on GitHub.

Top comments (0)