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
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.