# How to Integrate Custom Export Logic into the DESIGN.md CLI

> Learn to integrate custom export logic into the DESIGN.md CLI using its pluggable emitter architecture. Implement execute method, register handler, and validate output with Zod.

- Repository: [Google Labs Code/design.md](https://github.com/google-labs-code/design.md)
- Tags: how-to-guide
- Published: 2026-07-02

---

**The DESIGN.md CLI supports custom export formats through a pluggable emitter architecture that requires implementing an `execute(state)` method, registering your handler in the CLI dispatcher, and validating output with Zod schemas.**

The google-labs-code/design.md repository provides a CLI tool for managing design systems through markdown-based specifications. When the built-in export formats—Tailwind v3/v4, CSS variables, and DTCG—do not meet your project requirements, you can **integrate custom export logic** by leveraging the project's emitter plugin system. This architecture allows you to transform the internal `DesignSystemState` into any target format while maintaining type safety through validation.

## Understanding the Pluggable Emitter Architecture

The CLI processes `npx @google/design.md export` commands through a dispatcher that follows a strict three-step pipeline. First, it parses the [`DESIGN.md`](https://github.com/google-labs-code/design.md/blob/main/DESIGN.md) file into a `DesignSystemState` object defined in [`packages/cli/src/linter/model/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/spec.ts). Then, it selects an emitter based on the `--format` flag. Finally, it runs the emitter's `execute(state)` method, which must return a result matching a Zod-validated schema.

### The Core Emitter Contract

Built-in emitters demonstrate the required implementation pattern. The `TailwindEmitterSpec` interface in [`packages/cli/src/linter/tailwind/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/spec.ts) establishes the contract your custom emitter must follow:

```ts
// packages/cli/src/linter/tailwind/spec.ts
export interface TailwindEmitterSpec {
  execute(state: DesignSystemState): TailwindEmitterResult;
}

```

Your custom emitter must implement this interface, accepting the full design system state and returning a structured result. The `DesignSystemState` contains typed maps for colors, typography, spacing, and other design tokens extracted from your markdown files.

### Result Validation with Zod

The CLI enforces output consistency through the `TailwindEmitterResultSchema` located in [`packages/cli/src/linter/tailwind/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/spec.ts). This Zod schema uses a discriminated union pattern to validate success and error states:

```ts
// packages/cli/src/linter/tailwind/spec.ts
export const TailwindEmitterResultSchema = z.discriminatedUnion('success', [ … ]);

```

Custom emitters must define equivalent schemas to ensure the CLI can safely serialize and output results.

## Step-by-Step: Integrate Custom Export Logic

To add a completely new export format—such as `my-format`—follow these implementation steps derived from the source code structure.

### 1. Define the Specification

Create a spec file that mirrors the Tailwind implementation. Define a Zod schema for your output and an interface with an `execute(state)` method. Store this in [`packages/cli/src/linter/myformat/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/myformat/spec.ts):

```ts
// packages/cli/src/linter/myformat/spec.ts
import { z } from 'zod';
import type { DesignSystemState } from '../model/spec.js';

export const MyFormatResultSchema = z.object({
  success: z.literal(true),
  data: z.object({
    // Example: a flat map of token names → raw values
    tokens: z.record(z.string()),
  }),
});

export type MyFormatResult = z.infer<typeof MyFormatResultSchema>;

export interface MyFormatEmitterSpec {
  execute(state: DesignSystemState): MyFormatResult;
}

```

### 2. Implement the Handler

Create a handler class that transforms `DesignSystemState` into your custom representation. Reference [`packages/cli/src/linter/tailwind/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/handler.ts) as a template for accessing state properties:

```ts
// packages/cli/src/linter/myformat/handler.ts
import type { MyFormatEmitterSpec, MyFormatResult } from './spec.js';
import type { DesignSystemState } from '../model/spec.js';

export class MyFormatEmitterHandler implements MyFormatEmitterSpec {
  execute(state: DesignSystemState): MyFormatResult {
    const tokens: Record<string, string> = {};

    // Flatten colors (example)
    for (const [name, color] of state.colors) {
      tokens[`color.${name}`] = color.hex;
    }
    // Add other token types as needed …
    return {
      success: true,
      data: { tokens },
    };
  }
}

```

### 3. Register in the CLI Dispatcher

Locate the export command handler in [`packages/cli/src/cli.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/cli.ts) or the format parsing utility in [`packages/cli/src/utils.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/utils.ts). Import your handler and add a case to the format selection logic:

```ts
// packages/cli/src/cli.ts (excerpt)
import { MyFormatEmitterHandler } from './linter/myformat/handler.js';
import { MyFormatResultSchema } from './linter/myformat/spec.js';

...
case 'my-format': {
  const handler = new MyFormatEmitterHandler();
  const result = handler.execute(state);
  // Validation ensures a consistent shape
  MyFormatResultSchema.parse(result);
  console.log(JSON.stringify(result, null, 2));
  break;
}
...

```

### 4. Export from Module Index

Create an index file at [`packages/cli/src/linter/myformat/index.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/myformat/index.ts) to expose your spec and handler for clean imports:

```ts
export { MyFormatEmitterHandler } from './handler.js';
export { MyFormatResultSchema, type MyFormatEmitterSpec } from './spec.js';

```

Once registered, invoke your new format:

```bash
npx @google/design.md export --format my-format DESIGN.md > my-design.json

```

## Key Source Files for Custom Emitters

Understanding the codebase structure helps when you **integrate custom export logic**. These files define the core types and utilities:

- **[`packages/cli/src/linter/model/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/spec.ts)** – Defines `DesignSystemState`, the central data structure containing design tokens and component definitions.
- **[`packages/cli/src/linter/tailwind/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/spec.ts)** – Template for emitter specifications and Zod result schemas including `TailwindEmitterResultSchema`.
- **[`packages/cli/src/linter/tailwind/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/handler.ts)** – Reference implementation showing how to map state to output objects via `TailwindEmitterHandler`.
- **[`packages/cli/src/utils.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/utils.ts)** – Contains CLI argument parsing and format selection logic.
- **[`packages/cli/src/cli.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/cli.ts)** – Entry point where emitters are instantiated and executed.
- **[`packages/cli/src/version.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/version.ts)** – Contains the CLI version constant used by `--version`.

## Summary

- The DESIGN.md CLI uses a **pluggable emitter architecture** that parses markdown into `DesignSystemState` before routing to format-specific handlers.
- To add custom export logic, implement the `TailwindEmitterSpec` interface pattern with an `execute(state)` method in `packages/cli/src/linter/<format>/handler.ts`.
- Define a **Zod schema** for result validation to ensure the CLI receives predictable output shapes.
- Register your format in the CLI dispatcher at [`packages/cli/src/cli.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/cli.ts) by mapping the `--format` flag value to your handler instance.
- Reference the built-in Tailwind emitters in `packages/cli/src/linter/tailwind/` as working templates for contract implementation.

## Frequently Asked Questions

### What interface must my custom emitter implement?

Your custom emitter must implement an interface matching the pattern found in `TailwindEmitterSpec` from [`packages/cli/src/linter/tailwind/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/spec.ts). This requires a single method `execute(state: DesignSystemState)` that returns a Zod-validated result object containing at minimum a `success` boolean and a `data` payload.

### Where do I register a new export format in the CLI?

Register new formats in the CLI dispatcher located in [`packages/cli/src/cli.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/cli.ts) or within the format parsing utilities in [`packages/cli/src/utils.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/utils.ts). Add a case statement that maps your custom `--format` flag value (e.g., `my-format`) to a new instance of your emitter handler class.

### How does the CLI validate custom emitter output?

The CLI validates output using Zod schemas defined alongside your emitter spec. Your result schema—similar to `TailwindEmitterResultSchema` in [`packages/cli/src/linter/tailwind/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/tailwind/spec.ts)—must validate the shape returned by your `execute()` method. The dispatcher calls `.parse()` on this schema before serializing output.

### Can I export to formats other than JSON?

Yes. While the built-in emitters primarily produce JSON, your custom handler can generate any text-based format including XML, YAML, CSS, or proprietary formats. Simply adjust the `data` structure in your Zod schema and modify the serialization logic in your handler's `execute()` method to return strings in your target format.