Add a CLI Command
End-to-end workflow for adding a new command to cx. Every command falls into one of two archetypes - determine which one first, then follow the corresponding steps.
docs/adding-a-command.md has copy-pasteable code templates for every step below. Read it alongside this workflow.
Step 0: Understand What You're Building
Before writing any code, get clarity on the domain:
- What Coralogix API are you wrapping? Find the API docs or example responses. Understand the data model - what entities exist, what fields they have, what operations are supported.
- What should the user be able to do? List the subcommands (e.g.,
list,get,create) and what flags make sense. - Does this belong under a wrapper group? The CLI organizes related commands into wrapper groups. Check if your command fits under an existing group before creating a top-level command:
cx alerts- alert definitions +schedulerscx notifications-connectors,routers,presets,testcx webhooks- outgoing webhooks +actionscx enrichments- enrichment rules +customenrichment tablescx integrations- integrations +extensions,contextual-datacx iam-api-keys,roles,scopes,users,groups,ip-accessRuncx schemato see the full command tree as JSON.
- Which archetype fits?
| Archetype | When to use | Reference implementation |
|---|---|---|
| A: DataPrime-based | Querying logs, spans, or any DataPrime source | src/commands/logs/mod.rs |
| B: REST-based | Wrapping a Coralogix REST API (most new commands) | src/commands/alerts/api.rs + src/commands/alerts/mod.rs |
DataPrime commands delegate to a shared pipeline and require minimal code (~130 lines). REST commands build the full pipeline (API client, fan-out, merge, render) - more code but more control.
Important: All API integrations must use REST (HTTP). The CLI is HTTP-only by design - do not use gRPC.
Step 1: Read Reference Implementations
Before writing any code, read these files to internalize the existing patterns. This step is critical - agents that read existing code first produce implementations that are consistent with the codebase rather than inventing new patterns.
Always read:
src/main.rs- study theCommandsenum to see how variants are structured, and thematch cli.commanddispatch block to see where your new variant fits. Note which commands early-exit (no credentials needed) vs which go through the full config resolution flow. Pay attention to wrapper groups (e.g.,Notifications,Iam,Webhooks,Integrations) - these are top-level commands with nested subcommand enums that group related domains. If your command belongs under an existing group, add a new variant to that group's subcommand enum rather than creating a top-level command.src/commands/mod.rs- see existing module registrations so you add yours in the right placedocs/adding-a-command.md- full guide with code templates for both archetypes
DataPrime archetype - also read:
src/commands/logs/mod.rs- a complete DataPrime command; notice how little code is needed because the shared pipeline does the heavy liftingsrc/commands/dataprime/mod.rs- the shared pipeline your command will delegate to; understand therun_query()signature and what it handles (fan-out, merge, spilling, agents output)
REST archetype - also read:
src/commands/alerts/api.rs- see how response types are structured, how the API struct borrows&CxClient, how deserialization tests are writtensrc/commands/alerts/mod.rs- see how the handler declarespub mod api;and imports types viause api::{...};src/commands/dashboards/mod.rs- see the fan-out/merge/render pattern usingrender::*helpers, and how all three output formats are handled
Step 2: Create API Layer (REST Only)
Skip this step for DataPrime commands - they use the shared DataPrime pipeline.
Create src/commands/<domain>/api.rs. See docs/adding-a-command.md § "Archetype B, Step 1" for the full template.
Key conventions and why they matter:
#[serde(rename_all = "camelCase")]on response types - Coralogix APIs use camelCase JSON keys#[serde(default)]onVecfields - the API sometimes omits empty arrays entirely rather than sending[], so this prevents deserialization failuresOption<T>for fields that may be absent - be defensive, APIs evolve and fields vary across tiers- API struct borrows
&CxClient(don't own it) - the client is shared across the fan-out and must outlive individual API calls const BASE_PATHfor the endpoint prefix - keeps URLs DRY- Deserialization tests are mandatory - test both happy-path and edge cases (empty lists, missing optional fields) since these are the cases that break in production
Step 3: Create Command Module
Create src/commands/<domain>/mod.rs. For REST commands, declare pub mod api; at the top so the handler can use api::{...}; types from its sibling api.rs. See docs/adding-a-command.md for full templates of both archetypes.
DataPrime archetype
Provide two things:
- A text renderer:
pub fn render_<domain>_text(merged: &MergedResults) -> Result<()>- called only forOutputFormat::Text; JSON and Agents output are handled by the shared pipeline - A thin
run()wrapper that callssuper::dataprime::run_query()with your DataPrime source name
REST archetype
Build the full fan-out/merge/render pipeline. Key patterns to understand:
render::render_tablefor text output - pass column headers (without "Profile") and rows where the first element is the profile name. The helper conditionally includes the Profile column based oninclude_profile. No duplicate struct definitions needed.render::render_jsonfor JSON output - pretty-prints a&[Value]arraylet include_profile = targets.len() > 1;- this single boolean controls all multi-profile behavior (Profile column in text,"profile"key in JSON)- Fan-out errors are non-fatal - print to stderr and continue, because one misconfigured profile shouldn't block results from others
- Status messages go to stderr (
eprintln!) - stdout is reserved for data so piped output isn't polluted - Agents output is command-owned - each command calls
toon_encodedirectly after any post-processing, because different commands may transform data differently before encoding
Register the module in src/commands/mod.rs.
Step 4: Wire into CLI
In src/main.rs, add three things. See docs/adding-a-command.md § "CLI Wiring" for templates.
Commandsenum variant - DataPrime commands use inline args; REST commands reference a subcommand enum- Subcommand enum (REST only) - defines
List,Get, etc. - Dispatch match arm - inside the
match cli.commandblock. Most commands go through the full config resolution flow; only commands that don't need credentials (likeprofiles,cleanup) early-exit.
Step 5: Add Tests
Every new command must add tests at three layers. See
docs/adding-a-command.md § "Testing" for code templates and examples
of each.
| Layer | Location | What it verifies |
|---|---|---|
| Unit | src/**/<file>.rs #[cfg(test)] |
Pure logic - deserialization (mandatory for REST), helpers, transforms |
| Integration | tests/<command>/main.rs (wiremock) |
Command runner end-to-end with mocked HTTP |
| E2E | tests/e2e/<command>/mod.rs (assert_cmd, #[ignore]d) |
Real cx binary against the Coralogix test team |
Things specific to this workflow that the doc doesn't emphasise:
- Don't add e2e for mutating commands (create/delete/enable/disable)
unless there's a paired-undo plan - they touch shared test team state.
Mark them as deliberately uncovered with a comment, like
tests/e2e/alerts/mod.rs. - If a subcommand needs an ID from the test team (e.g.
get <id>), add a localdiscover_*fn in your e2e test module, modelled afterdiscover_alert_idintests/e2e/alerts/mod.rs. Cache viaOnceLockand skip gracefully when the test team has no data - don't panic. - Don't forget to declare the new e2e module in
tests/e2e.rsvia#[path = "e2e/your_domain/mod.rs"] mod your_domain;.
Step 6: Create User-Facing Skill
Every command needs a corresponding skill in skills/ so AI agents know how to use it. Use the add-skill workflow to create it - it walks through the full process including reading reference implementations, writing effective trigger descriptions, and verification.
Step 7: Verify
Run cargo build, cargo test (unit + integration), cargo clippy,
and cargo fmt --check. Fix any issues before committing.
If you have test team credentials configured, also run the e2e suite:
cargo test --test e2e -- --ignored --test-threads=1
Smoke test all three output formats (text, json, agents) and
multi-profile (-p profile1 -p profile2).
See docs/adding-a-command.md § "PR Checklist" for the full checklist to include in your PR description.