Why CLIs matter in AI engineering
The command-line interface (CLI) remains one of the most powerful tools for automation, scripting workflows, and orchestrating complex processes — a foundation that continues to endure even as development paradigms evolve.
In AI and machine learning workflows, CLIs serve as the connective tissue between automation, pipelines, batch training jobs, and deployment tasks, often operating in headless or CI environments.
With the rise of agentic systems — AI agents that execute steps, collaborate, and are orchestrated across workflows — the CLI is becoming a critical interface for both human engineers and AI agents. It must therefore be automatable, scriptable, and predictable.
Finally, thoughtful CLI design directly improves developer experience (DX): enabling faster onboarding, reducing friction, and encouraging reuse while minimizing errors — qualities that are essential for scaling AI engineering effectively.
This is the Age of APIs, MCPs, and CLIs. Same functionality, different interfaces. Here’s what I learned redesigning my CLI tool.
TL;DR
- Building CLIs and APIs? API is primary, CLI is secondary.
- Every CLI command should map 1:1 to an API operation.
- Use consistent patterns:
Verb + Noun + [Qualifier]naming, typed Input/Output structs, structured errors, and resource hierarchies. - Design your API first, then wrap it with a thin CLI layer.
1. Core Philosophy: API First
Key Insight: CLI is NOT the primary interface. It’s a convenience wrapper around your API.
User/Application
├── CLI Layer (til capture) → Maps to
└── Go SDK (api.Capture) → Core API Layer
Semantic Consistency: Same parameters, same semantics, different syntax.
# CLI
til capture start --session-id sess_123 --title "Bug Fix"
# SDK (Go)
api.Capture.Start(ctx, &CaptureStartInput{
SessionID: "sess_123",
Title: "Bug Fix",
})
2. Essential API Patterns
2.1 Operation Naming?
Remember AWS Pattern?
# CLI
aws <service> <operation> [--param value ...]
Now let’s extract core idea here i.e. Every service operation maps to a single command pattern:
Verb + Noun + [Qualifier]
Examples:
// Good
api.Session.Create()
api.Session.Get()
api.Session.List()
// Bad
api.Session.New() // Use Create
api.Session.Fetch() // Use Get
2.2. Typed Input/Output Structs
Every operation needs typed structs for extensibility:
type CaptureStartInput struct {
SessionID string // Optional: auto-generate
Title string
UserID string // Required
Tags []string
}
type CaptureStartOutput struct {
SessionID string
StartedAt time.Time
}
func (c *CaptureAPI) Start(ctx context.Context, input *CaptureStartInput) (*CaptureStartOutput, error)
2.3 Structured Errors
Machine-readable codes + human-readable messages:
type TILError struct {
Code string // "ResourceNotFoundException"
Message string // "Session sess_123 not found"
StatusCode int // 404
Details map[string]string
}
2.4 Consistent Pagination
type PaginationInput struct {
Limit int // Max results (default: 50)
NextToken string // Cursor for next page
}
type ListSessionsOutput struct {
Sessions []Session
NextToken string
}
3. Resource Hierarchy
A well-defined hierarchy:
- Reflects ownership and lifecycle coupling between resources.
→ e.g., Events can’t exist without Messages; Messages belong to Sessions. - Enables predictable URL patterns and intuitive CLI commands.
→ e.g., cli session msg list --session sess_123vs.GET /sessions/sess_123/messages. - Facilitates permissions, pagination, and cleanup.
→ e.g., deleting a session can cascade or archive its messages/artifacts.
Allows you to design consistent CRUD verbs and hierarchical discovery patterns (similar to AWS or GCP).
Session (sess_*) - A complete AI conversation/task
├── Messages (msg_*) - User prompts & AI responses
│ ├── Events (evt_*) - What happened during generation
│ │ ├── ToolCall - Function/API calls
│ │ ├── Reasoning - Chain-of-thought
│ │ └── Retry - Error recovery
│ └── Attachments (att_*) - Images, documents
├── Artifacts (art_*) - Generated code, documents
│ └── Versions (v1, v2...) - Edit history
├── Analysis (ana_*) - Post-processing results
│ ├── Hallucination scores
│ ├── Token usage & costs
│ └── Performance metrics
└── Blobs (blob_*) - Content-addressed storage
└── Used for deduplication across sessions
API operations follow the hierarchy:
api.Session.Create(ctx, input)
api.Message.ListBySession(ctx, sessionID)
api.Event.ListByMessage(ctx, messageID)
4. CLI Design for AI Engineers
4.1 Commands That Match Mental Models
# Resource-first (AWS style)
til session create --title "My Session"
til session get sess_123
# NOT action-first
til create session --title "My Session"
4.2 Common Flags
--format json|table|yaml # Output format
--quiet # Suppress output
--dry-run # Preview changes
--force # Skip confirmations
4.3 Output Formats
Human-friendly table (default):
$ til session list
SESSION ID TITLE STATUS STARTED
sess_123 Bug Fix active 2m ago
sess_456 Feature completed 1h ago
Machine-friendly JSON:
$ til session list --format json
{"sessions": [...], "next_token": ""}
Interactive Safety
$ til session delete sess_123
⚠️ This will permanently delete session sess_123
Continue? (y/N):
Key Takeaways
- API is the contract - CLI is just one consumer
- Consistency matters - Same patterns everywhere
- Type everything - Input/Output structs for all operations
- Resource hierarchy - Reflects real-world relationships
- Error codes - Machine-readable + human-friendly
- Safety first - Confirmations for destructive actions
Building something similar? Start with the API design. The CLI will naturally follow.