Skip to main content

Overview

Incur normalizes all command output into a structured envelope with consistent shape. The envelope includes:
  • Data — the command’s return value
  • Metadata — command name, duration, and CTAs
  • Error details — when the command fails
By default, incur uses TOON format for output, which is up to 60% more token-efficient than JSON for agents.

Output Envelope Structure

Every command produces an output envelope with a consistent structure.

Success Envelope

{
  ok: true,
  data: <command return value>,
  meta: {
    command: "my-cli deploy",
    duration: "42ms",
    cta?: { ... }  // optional
  }
}

Error Envelope

{
  ok: false,
  error: {
    code: "NOT_AUTHENTICATED",
    message: "Token not found",
    retryable: false,  // optional
    fieldErrors: [...]  // optional, for validation errors
  },
  meta: {
    command: "my-cli deploy",
    duration: "12ms",
    cta?: { ... }  // optional
  }
}

Return Values

By default, commands return data directly:
cli.command('status', {
  run() {
    return { clean: true, branch: 'main' }
  },
})
$ my-cli status
clean: true
branch: main
Use --verbose to see the full envelope:
$ my-cli status --verbose
ok: true
data:
  clean: true
  branch: main
meta:
  command: status
  duration: 8ms

Using c.ok() and c.error()

Use c.ok() and c.error() to attach CTAs or signal structured errors.

c.ok()

Return success with optional metadata:
cli.command('deploy', {
  run(c) {
    return c.ok(
      { deployed: true, url: 'https://staging.example.com' },
      {
        cta: {
          commands: [
            { command: 'logs', description: 'View deployment logs' },
            { command: 'status', description: 'Check status' },
          ],
        },
      }
    )
  },
})
$ my-cli deploy
deployed: true
url: https://staging.example.com

Suggested commands:
  my-cli logs  # View deployment logs
  my-cli status  # Check status

c.error()

Return a structured error:
cli.command('deploy', {
  run(c) {
    if (!authenticated()) {
      return c.error({
        code: 'NOT_AUTHENTICATED',
        message: 'Token not found',
        retryable: false,
        cta: {
          commands: [
            { command: 'login', description: 'Log in to continue' },
          ],
        },
      })
    }
    return { deployed: true }
  },
})
$ my-cli deploy
Error (NOT_AUTHENTICATED): Token not found

Suggested commands:
  my-cli login  # Log in to continue

Call-to-Actions (CTAs)

CTAs tell users (human or agent) what commands to run next. They appear after successful or failed commands.

String CTAs

Simplest form — just the command name:
return c.ok(
  { items: [...] },
  {
    cta: {
      commands: ['get 1', 'list closed'],
    },
  }
)
Suggested commands:
  my-cli get 1
  my-cli list closed

Object CTAs

Provide structured arguments and options with descriptions:
return c.ok(
  { items: [...] },
  {
    cta: {
      commands: [
        {
          command: 'get',
          args: { id: 1 },
          description: 'View item details',
        },
        {
          command: 'list',
          args: { state: 'closed' },
          description: 'View closed items',
        },
      ],
    },
  }
)
Suggested commands:
  my-cli get 1  # View item details
  my-cli list closed  # View closed items

Type-Safe CTAs

CTAs are fully type-inferred. If your CLI has registered commands, the command field is typed as a union of command names:
cli
  .command('get', { ... })
  .command('list', { ... })
  .command('deploy', {
    run(c) {
      return c.ok(
        { deployed: true },
        {
          cta: {
            commands: [
              { command: 'get' },     // ✅ typed as 'get' | 'list' | 'deploy'
              { command: 'status' },  // ❌ type error if 'status' isn't registered
            ],
          },
        }
      )
    },
  })

Output Formats

Incur supports multiple output formats. Use --format <fmt> or --json to override the default.

TOON (default)

TOON is a token-efficient format designed for agents. It strips braces, quotes, and redundant keys.
$ my-cli status
clean: true
branch: main
files[2]: a.txt,b.txt

JSON

Standard JSON with indentation:
$ my-cli status --json
{
  "clean": true,
  "branch": "main",
  "files": ["a.txt", "b.txt"]
}

YAML

YAML format:
$ my-cli status --format yaml
clean: true
branch: main
files:
  - a.txt
  - b.txt

Markdown

Tables for flat objects and arrays:
$ my-cli users --format md
| id | name  | email           |
|----|-------|------------------|
| 1  | Alice | [email protected]|
| 2  | Bob   | [email protected]  |

JSONL

Newline-delimited JSON, useful for streaming:
$ my-cli logs --format jsonl
{"type":"chunk","data":"connecting..."}
{"type":"chunk","data":"streaming logs"}
{"type":"done","ok":true}

Format Priority

Formats are resolved in this order:
  1. Explicit --format or --json flag
  2. Command-level format option
  3. CLI-level format option
  4. Default (toon)
Cli.create('my-cli', {
  format: 'json',  // CLI-level default
})
  .command('deploy', {
    format: 'yaml',  // Overrides CLI default
    run() {
      return { deployed: true }
    },
  })
$ my-cli deploy
# Uses YAML (command-level override)

$ my-cli deploy --json
# Uses JSON (flag override)

Output Policy

Control when output data is displayed with outputPolicy.

'all' (default)

Displays to both humans and agents:
cli.command('deploy', {
  outputPolicy: 'all',  // default
  run() {
    return { deployed: true }
  },
})

'agent-only'

Suppresses data in TTY mode, but still returns it to agents:
cli.command('deploy', {
  outputPolicy: 'agent-only',
  run() {
    // Agents get the data; humans see nothing (unless --verbose or --json)
    return { id: 'deploy-123', url: 'https://staging.example.com' }
  },
})
$ my-cli deploy
# (no output in terminal)

$ my-cli deploy --json
{"id":"deploy-123","url":"https://staging.example.com"}
Inheritance:
  • CLI-level outputPolicy applies to all commands
  • Group-level outputPolicy overrides CLI-level
  • Command-level outputPolicy overrides group-level

Streaming Output

Use async *run to stream chunks incrementally:
cli.command('logs', {
  async *run() {
    yield 'connecting...'
    yield 'streaming logs'
    yield { progress: 50 }
    yield { progress: 100 }
    return c.ok(undefined, {
      cta: {
        commands: [{ command: 'status', description: 'Check status' }],
      },
    })
  },
})
With default TOON format, each chunk is printed as a line:
$ my-cli logs
connecting...
streaming logs
progress: 50
progress: 100

Suggested commands:
  my-cli status  # Check status
With --format jsonl, each chunk becomes a structured event:
$ my-cli logs --format jsonl
{"type":"chunk","data":"connecting..."}
{"type":"chunk","data":"streaming logs"}
{"type":"chunk","data":{"progress":50}}
{"type":"chunk","data":{"progress":100}}
{"type":"done","ok":true,"meta":{"command":"logs","duration":"42ms"}}

Error Handling

Errors are automatically captured and normalized:

Thrown Errors

run() {
  throw new Error('Something went wrong')
}
$ my-cli deploy
Error: Something went wrong

Structured Errors

Use c.error() for custom error codes:
run(c) {
  return c.error({
    code: 'DEPLOY_FAILED',
    message: 'Could not connect to server',
    retryable: true,
  })
}
$ my-cli deploy --verbose
ok: false
error:
  code: DEPLOY_FAILED
  message: Could not connect to server
  retryable: true
meta:
  command: deploy
  duration: 12ms

Validation Errors

Validation errors include field-level details:
$ my-cli deploy
Error: missing required argument <env>
With --verbose:
$ my-cli deploy --verbose
ok: false
error:
  code: VALIDATION_ERROR
  message: Required
  fieldErrors:
    - path: env
      expected: string
      received: undefined
      message: Required
meta:
  command: deploy
  duration: 3ms

Next Steps

CLI Creation

Learn how to create and configure CLIs

Middleware

Explore middleware for composable hooks

Build docs developers (and LLMs) love