Skip to main content
Without CTAs (call-to-actions), agents have to guess what to do next or ask the user. With CTAs, your CLI tells the agent exactly which commands are relevant after each run, so it can chain operations without extra prompting.

Basic Usage

Return CTAs from c.ok() to suggest next steps:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('list', {
    description: 'List items',
    run(c) {
      const items = [{ id: 1, title: 'Fix bug' }]
      return c.ok(
        { items },
        {
          cta: {
            commands: [
              { command: 'get 1', description: 'View item' },
              { command: 'create', description: 'Add new item' },
            ],
          },
        },
      )
    },
  })
  .serve()
Output
$ my-cli list
items:
  - id: 1
    title: Fix bug
Next:
  my-cli get 1 View item
  my-cli create Add new item

Type-Safe CTAs

CTA parameters are fully type-inferred, so agents get valid command names, arguments, and options:
import { Cli, z } from 'incur'

const cli = Cli.create('my-cli', { description: 'My CLI' })
  .command('list', {
    args: z.object({ 
      state: z.enum(['open', 'closed']).default('open') 
    }),
    run(c) {
      const items = [{ id: 1, title: 'Fix bug' }]
      return c.ok(
        { items },
        {
          cta: {
            commands: [
              { 
                command: 'get', 
                args: { id: 1 }, // Type-safe args
                description: 'View item' 
              },
              { 
                command: 'list', 
                args: { state: 'closed' }, // Validates against enum
                description: 'View closed' 
              },
            ],
          },
        },
      )
    },
  })
  .command('get', {
    args: z.object({ id: z.number() }),
    run(c) {
      return { id: c.args.id, title: 'Fix bug', state: 'open' }
    },
  })

cli.serve()
Output
$ my-cli list
items:
  - id: 1
    title: Fix bug
Next:
  my-cli get 1 View item
  my-cli list closed View closed

CTAs with Options

Suggest commands with both arguments and options:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('deploy', {
    args: z.object({ 
      env: z.enum(['staging', 'production']) 
    }),
    options: z.object({ 
      force: z.boolean().optional() 
    }),
    run(c) {
      return c.ok(
        { deployed: true, env: c.args.env },
        {
          cta: {
            commands: [
              { 
                command: 'status', 
                description: 'Check deployment' 
              },
              { 
                command: 'deploy', 
                args: { env: 'production' },
                options: { force: true },
                description: 'Deploy to production with force' 
              },
            ],
          },
        },
      )
    },
  })
  .command('status', {
    run() {
      return { status: 'running' }
    },
  })
  .serve()
Output
$ my-cli deploy staging
deployed: true
env: staging
Next:
  my-cli status Check deployment
  my-cli deploy production --force Deploy to production with force

Short-Form CTAs

Use plain strings for simple command suggestions:
import { Cli } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('init', {
    run(c) {
      return c.ok(
        { initialized: true },
        {
          cta: {
            commands: ['build', 'deploy staging'],
          },
        },
      )
    },
  })
  .serve()
Output
$ my-cli init
initialized: true
Next:
  my-cli build
  my-cli deploy staging

Custom CTA Labels

Customize the “Next:” label:
import { Cli } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('analyze', {
    run(c) {
      return c.ok(
        { issues: 3 },
        {
          cta: {
            description: 'Recommended actions:',
            commands: [
              { command: 'fix', description: 'Auto-fix issues' },
              { command: 'report', description: 'Generate report' },
            ],
          },
        },
      )
    },
  })
  .serve()
Output
$ my-cli analyze
issues: 3
Recommended actions:
  my-cli fix Auto-fix issues
  my-cli report Generate report

Error CTAs

Suggest recovery commands after errors:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('deploy', {
    args: z.object({ env: z.enum(['staging', 'production']) }),
    run(c) {
      const authenticated = false
      
      if (!authenticated) {
        return c.error({
          code: 'AUTH',
          message: 'Not authenticated',
          cta: {
            commands: [
              { command: 'login', description: 'Log in to continue' },
            ],
          },
        })
      }
      
      return { deployed: true }
    },
  })
  .command('login', {
    run() {
      return { authenticated: true }
    },
  })
  .serve()
Output
$ my-cli deploy staging
Error (AUTH): Not authenticated
Next:
  my-cli login Log in to continue

Agent Behavior

CTAs appear in all output formats: TOON (default)
$ my-cli list
items:
  - id: 1
Next:
  my-cli get 1
JSON
$ my-cli list --json
{
  "ok": true,
  "data": {
    "items": [{ "id": 1 }]
  },
  "meta": {
    "cta": {
      "commands": [
        {
          "command": "get",
          "args": { "id": 1 }
        }
      ]
    }
  }
}
Agents can parse the meta.cta structure to determine the next command to run.

Conditional CTAs

Generate CTAs based on the result:
import { Cli, z } from 'incur'

Cli.create('my-cli', { description: 'My CLI' })
  .command('status', {
    run(c) {
      const hasChanges = true
      const branch = 'feature-branch'
      
      const commands: any[] = []
      
      if (hasChanges) {
        commands.push({ 
          command: 'commit', 
          description: 'Commit changes' 
        })
      }
      
      if (branch !== 'main') {
        commands.push({ 
          command: 'merge main', 
          description: 'Merge from main' 
        })
      }
      
      return c.ok(
        { branch, hasChanges },
        commands.length > 0 ? { cta: { commands } } : undefined,
      )
    },
  })
  .serve()
Output
$ my-cli status
branch: feature-branch
hasChanges: true
Next:
  my-cli commit Commit changes
  my-cli merge main Merge from main

Implementation Details

CTAs are part of the output envelope and appear in the meta section when using --verbose or --json:
type OutputEnvelope = {
  ok: boolean
  data: unknown
  meta?: {
    cta?: {
      description?: string
      commands: Array<
        | string
        | {
            command: string
            args?: Record<string, unknown>
            options?: Record<string, unknown>
            description?: string
          }
      >
    }
  }
}
In TTY mode (human terminal), CTAs are formatted as a “Next:” section. In non-TTY mode (agent), they’re included in the JSON envelope for programmatic parsing.
Use CTAs to guide agents through multi-step workflows without requiring them to prompt the user for next steps.

Build docs developers (and LLMs) love