Mitc.ConnectWiseCli 0.4.12
cw — ConnectWise PSA CLI
cw is a personal-productivity command-line tool for ConnectWise PSA. It wraps a curated slice of the ConnectWise REST API, covering the things a billable team member needs daily: browsing projects, searching and updating tickets (both project tickets and service tickets), looking up companies, and viewing and logging your own time.
Everything the CLI does is authenticated as you — via your API key pair — and is therefore constrained by your ConnectWise security role. No separate service account is required.
Getting Your ConnectWise API Keys
API keys are generated from inside ConnectWise under your own member account. You need three pieces of information before running cw config set:
| Piece | Where to get it |
|---|---|
| Public key | ConnectWise → My Account → API Keys → New → copy the Public Key |
| Private key | Same dialog — copy the Private Key immediately; ConnectWise never shows it again |
Your security role must permit API key generation. If the API Keys tab is missing from My Account, ask your ConnectWise administrator to enable it for your role.
Authentication uses HTTP Basic auth in the form company+publicKey:privateKey, sent with every request alongside a clientId header. There is no username/password login flow and no token refresh — every call is self-contained.
Installation
From the M&I NuGet feed (recommended)
cw is published as a .NET global tool on M&I's private NuGet feed, which requires authentication. Configuring the feed (its URL, credentials, and how to register it as a NuGet source on your machine) is documented in the M&I NuGet server article in IT Glue.
Once the feed is configured as a package source, install (and later upgrade) the tool with:
dotnet tool install Mitc.ConnectWiseCli --global
dotnet tool update Mitc.ConnectWiseCli --global
This puts the cw command on your PATH. Requires the .NET SDK.
From source
dotnet publish -c Release -o ./publish
Add the publish directory to your PATH, or create an alias cw pointing to ConnectWiseCli.exe.
Configuration
Run cw config set once to create the config file:
cw config set \
--public-key "AbCdEf1234" \
--private-key "xYzSecretKey" \
--member "jdoe"
All config set options:
| Option | Required | Description |
|---|---|---|
--instance |
No | ConnectWise hostname (default: clearit.mitcs.com) |
--client-id |
No | ConnectWise Developer Network integration ID (default: M&I's registered integration) |
--company |
No | ConnectWise company short name (default: mitc) |
--public-key |
Yes | API key public portion |
--private-key |
Yes | API key private portion |
--member |
Yes | Your ConnectWise member identifier (e.g. jdoe) |
--closed-statuses |
No | Comma-separated project status names hidden by default (default: Closed, Completed) |
--inactive-statuses |
No | Comma-separated company status names hidden by default (default: Former Client) |
Closed / inactive status names
cw projects search hides closed projects and cw companies search hides inactive companies by default. ConnectWise has no queryable "is closed" flag and its status setup tables aren't readable by the integration's security role, so the CLI excludes by status name. The names that count as closed/inactive are configurable because they vary per instance:
cw config set --closed-statuses "Closed,Completed,Won,Lost" --inactive-statuses "Former Client,Vendor/Other"
If you never set them, the defaults above apply. cw config show displays the effective values, marking unset lists as (default).
Config file location
The config is stored at:
%APPDATA%\ConnectWiseCli\config.json
It is plain JSON and is human-editable. It contains your private API key in plain text. Do not commit it to source control.
To inspect the current config (private key is always redacted in output):
cw config show
Global Flags
These flags apply to every command and must be placed before the subcommand:
| Flag | Description |
|---|---|
--json |
Emit raw JSON instead of formatted tables. Useful for piping to jq or other tools. Untouched by table-only presentation (per-project/time totals, summary/notes truncation, clickable ticket links, "No … matched." messages) — full field values, every time |
-v / --verbose |
Enable debug logging, including redacted HTTP request/response details |
Examples:
cw --json projects search --mine
cw -v tickets search "Sierra Nevada" --board "Internet Solutions Projects"
--select — extracting a single value
Unlike the flags above, --select <path> goes after the subcommand:
cw tickets get 956417 --select url
| Flag | Description |
|---|---|
--select <path> |
With --json, print just one property's value, raw and unquoted, so it drops straight into a shell substitution. Implies --json. Accepts a top-level key or a dotted path into nested objects (--select company.name); keys are case-sensitive and match the camelCase used in --json output. If the path doesn't resolve, or lands on an object or array instead of a scalar, the command exits non-zero with an error — pipe to jq for anything structured. |
url=$(cw tickets get 956417 --select url)
Commands
Convention — free-text search is a positional operand. Every searchable object (
projects,tickets,companies) exposes its free-text term as an optional positional argument on itssearchverb (e.g.cw tickets search "server down"), never as a--queryoption. Structured filters stay as--options. New searchable objects should follow this pattern, and each operand'sDescriptionshould state which field(s) it matches.
Convention — name filters match by substring, except
--status(prefix). The--boardand--type(companies) options, and the--companyfilter onprojects search/tickets search, match contains rather than exact equality (e.g.--board "Internet Solutions"matches"Internet Solutions Projects"). The--statusoption is the exception: it matches by prefix (starts-with,like "X%"), not substring — so--status "Clos"matches"Closed", but--status "Active"does NOT match"Inactive". Within the--companyfilter specifically, the company's short identifier is matched exactly rather than by substring (short codes over-match on contains), and it also accepts a numeric ConnectWise record id, matched exactly. The closed/inactive status exclusion is likewise always exact. (companies search's own free-text argument is a separate case — see below — and matches both name and identifier by contains.) If a search matches nothing, the CLI prints a plain "No <items> matched." line instead of an empty table. None of this affects--jsonoutput — it's always the full, untruncated, unlinked data with no added totals or empty-state message.
config
cw config set
Create or update the config file. All options are described in the Configuration section above.
cw config show
Display the current configuration. The private key is always shown as ********.
projects
cw projects search [name]
Search projects. The search term is an optional positional argument matching the project name; with no arguments, lists every project your security role can see. Results exclude closed projects by default — pass --include-closed to see them, or --status "Closed" to see only closed projects. Which status names count as closed is configurable (default: Closed, Completed).
cw projects search "Marketing Site"
cw projects search --mine
cw projects search --board "Internet Solutions Projects"
cw projects search --company "County of Glenn" --status "Open"
cw projects search --include-closed
| Argument / Option | Description |
|---|---|
[name] |
Free-text match on project name (positional) |
--mine |
Filter to projects where you are the manager |
--board |
Filter by board name (contains) |
--company |
Filter by company name (contains), identifier (exact), or numeric record id |
--status |
Filter by status name (prefix match, e.g. Open, Closed). Disables the default closed-project exclusion |
--include-closed |
Include closed projects (excluded by default) |
--conditions |
Raw ConnectWise conditions string (overrides the other filters) |
Output columns: Id, Name, Company, Status, Budget (hours), Actual (hours).
cw projects get <id>
Show full details for a single project.
cw projects get 1042
Output fields: Id, Name, Company, Board, Status, Manager, Budget Hours, Actual Hours.
tickets
cw tickets search [summary]
Search across project tickets and service tickets. The search term is an optional positional argument matching the ticket summary. By default, searches both types and excludes closed tickets. Pass --include-closed to include them, or --status "Closed" to see only closed tickets.
A search of two to five words matches each word independently, so the words need only all appear in the summary, in any order — backup failed finds "Backup job has failed" and "Failed the nightly backup". A single word, or a longer phrase (six or more words, assumed to be pasted text), is matched as one literal substring.
cw tickets search "email migration" --board "Internet Solutions Projects"
cw tickets search --mine --type service --status "In Progress"
cw tickets search --company "AM King" --conditions "summary contains \"server\""
cw tickets search --project "Marketing Site"
cw tickets search "email migration" --include-closed
| Argument / Option | Description |
|---|---|
[summary] |
Free-text match on ticket summary (positional). 2–5 words are AND-matched independently (any order); a single word or 6+ word phrase matches literally |
--type |
project, service, or both (default: both) |
--mine |
Filter to tickets assigned to you |
--company |
Filter by company name (contains), identifier (exact), or numeric record id |
--board |
Filter by board name (contains) |
--project |
Filter by parent project name (contains match). Implies project tickets only, since service tickets have no project |
--status |
Filter by status name (prefix match, e.g. --status "Clos" matches Closed). Disables the default closed-ticket exclusion |
--include-closed |
Include closed tickets (excluded by default) |
--conditions |
Raw ConnectWise conditions string (overrides the other filters) |
Output columns: Id, Type, Project, Summary, Company, Status, Budget, Actual. Budget and Actual are the ticket's budgeted and actual hours (blank when unset — common on service tickets). Rows are grouped by project (sorted by project name, then Id); service tickets, which have no project, appear last. In a table-view terminal that supports it, the Id is a clickable link straight to the ticket; long Summary text is truncated with an ellipsis (the full text is always in --json output).
If there's at least one result, a Per-project totals table follows, summing Budget, Actual, and Variance (Budget − Actual) per project — service tickets, which have no project, are rolled up under "(no project)". This table is table-view only; it's omitted from --json.
cw tickets get <id>
Show full details for a single ticket. Tries service tickets first, then project tickets.
cw tickets get 88421
Output fields: Id, Type, Summary, Company, Board, Status, Owner, Budget Hours, Actual Hours, URL.
The URL is a clickable ConnectWise link to the ticket — service and project tickets both open in the ticket form, addressed by the ticket id. It's included on every ticket in --json tickets search / get output too, so other tooling (e.g. timecard generation) can link straight to the ticket.
cw tickets update <id>
Update fields on a ticket. At least one of --status, --assign, --summary, or --set must be provided.
cw tickets update 88421 --status "In Progress" --assign "jdoe"
cw tickets update 88421 --summary "CSU Chico – email migration Phase 2"
cw tickets update 88421 --set "priority/name=High" --set "impact/name=Medium"
| Option | Description |
|---|---|
--type |
project or service. If omitted, resolved by fetching the ticket first |
--status |
New status name |
--assign |
Assignee member identifier (e.g. jdoe) |
--summary |
New summary text |
--set |
Raw JSON Patch path=value pair (repeatable). E.g. --set "priority/name=High" |
cw tickets time <id>
List all time entries logged against a ticket.
cw tickets time 88421
Output columns: Id, Member, Start, Hours, Billable, Notes. Start is shown in your local timezone (--json emits the raw UTC value ConnectWise stores). If there's at least one result, a Total: N hours line follows, summing the Hours column; this footer is table-view only and omitted from --json, same as cw time list.
cw tickets notes <id>
List the notes (the ticket's discussion log) on a ticket. Tries service tickets first, then project tickets — the same fallback as get.
cw tickets notes 88421
Output columns: Tab, Date, Author, Text. Date is shown in your local timezone (--json emits the raw UTC value ConnectWise stores). Notes are grouped by tab — Discussion, then Internal, then Resolution — oldest first within each tab. ConnectWise stores a note's tab membership as independent flags, so a note shown on more than one tab lists them together (e.g. Discussion, Internal); a note on none is labelled Other.
companies
cw companies search
Search companies by name or identifier. The search term is an optional argument; with no arguments, lists companies your security role can see. Inactive companies are excluded by default — pass --include-inactive to see them, or --status "<name>" to filter to a specific status. Which status names count as inactive is configurable (default: Former Client).
cw companies search Glenn
cw companies search "Sierra Nevada" --type Client
cw companies search --status Active
cw companies search Glenn --include-inactive
| Option | Description |
|---|---|
<query> |
Optional argument: matches company name or identifier (contains) |
--type |
Filter by company type (contains, e.g. Client, Vendor, Prospect) |
--status |
Filter by status name (prefix match, e.g. Active). Note that --status "Active" does NOT match Inactive — matching is by prefix, not substring. Disables the default inactive exclusion |
--include-inactive |
Include inactive companies (excluded by default) |
--conditions |
Raw ConnectWise conditions string (overrides the other filters) |
Output columns: Id, Identifier, Name, Status, Phone, City.
cw companies get <id-or-identifier>
Show full details for a single company. The argument can be either the numeric record id or the company identifier (the short code shown in the Identifier column of companies search).
cw companies get 250
cw companies get amking
A numeric argument tries the record id first; if no company has that id, it falls back to looking the value up as the identifier — so an all-digit identifier is still reachable even though numbers are tried as an id first. A non-numeric argument is always treated as the identifier.
Output fields: Id, Identifier, Name, Status, Type, Phone, Website, Street, City, State, Zip, Territory.
time
cw time list
List your own time entries. Defaults to the current calendar month.
cw time list
cw time list --today
cw time list --from 2026-06-01 --to 2026-06-30
cw time list --unbilled
| Option | Description |
|---|---|
--from |
Start date, inclusive (format: yyyy-MM-dd). Defaults to first day of current month |
--to |
End date, exclusive (format: yyyy-MM-dd). Defaults to first day of next month |
--today |
Scope to today's entries only. Cannot be combined with --from/--to |
--unbilled |
Show only entries not yet on an invoice |
Output columns: Id, Start, Hours, Charge (type + ticket id), Billable, WIP, Notes. Start is shown in your local timezone, and --from/--to are interpreted as local calendar days (--json emits the raw UTC values ConnectWise stores). Long Notes text is truncated with an ellipsis in table view (the full text is always in --json output). If there's at least one result, a Total: N hours line follows, summing the Hours column; this footer is table-view only and omitted from --json.
cw time add
Log a time entry against a ticket.
cw time add --ticket 88421 --start 9:00 --end 11:30 --notes "Deployed new Integral Ag build to staging"
cw time add --ticket 88421 --start 13:00 --end 14:00 --date 2026-06-20 --billable DoNotBill
| Option | Required | Description |
|---|---|---|
--ticket |
Yes | Ticket ID to log against |
--start |
Yes | Start time (e.g. 9:00, 13:30) |
--end |
Yes | End time (e.g. 11:30, 17:00) |
--date |
No | Date in yyyy-MM-dd format. Defaults to today |
--notes |
No | Time entry notes |
--type |
No | project or service. If omitted, resolved by fetching the ticket first |
--billable |
No | Billable option (e.g. Billable, DoNotBill). If omitted, ConnectWise applies the ticket's own billable default |
--work-type-id |
No | Work type ID. If omitted, ConnectWise applies the ticket's own default |
--work-role-id |
No | Work role ID. If omitted, ConnectWise applies the ticket's own default |
Hooks
cw can run a shell command after certain write operations succeed. Configure hooks in config.json under a hooks object:
{
"hooks": {
"time.added": "my-time-notifier.exe",
"ticket.updated": "my-ticket-webhook.bat"
}
}
Each value is a command line executed via cmd /c. The event context is serialized as JSON and piped to the command's standard input.
Supported events
| Event | Fired after | Context (stdin JSON) |
|---|---|---|
time.added |
cw time add succeeds |
The full TimeEntry object returned by the ConnectWise API |
ticket.updated |
cw tickets update succeeds |
{ "id": int, "type": string, "status": string, "assign": string, "summary": string } |
Failure behavior
Hooks are best-effort: if the hook command exits with a non-zero code or throws an exception, cw logs a warning and continues normally. A hook failure never blocks or fails the command that fired it.
Error Handling
- If no config file exists, commands that require ConnectWise access will print a clear message directing you to run
cw config set. - API errors surface as a problem message including the HTTP status and ConnectWise error code.
- Run with
-vto see the full request/response cycle (private key is never logged).
| Version | Downloads | Last updated |
|---|---|---|
| 0.4.13 | 0 | 7/14/2026 |
| 0.4.12 | 1 | 7/13/2026 |
| 0.4.11 | 1 | 7/10/2026 |
| 0.4.10 | 1 | 7/9/2026 |
| 0.4.9 | 2 | 7/6/2026 |
| 0.4.8 | 1 | 7/2/2026 |
| 0.4.7 | 1 | 7/1/2026 |
| 0.4.6 | 1 | 6/25/2026 |
| 0.4.5 | 1 | 6/25/2026 |
| 0.4.4 | 1 | 6/23/2026 |
| 0.4.3 | 1 | 6/23/2026 |
| 0.4.2 | 1 | 6/23/2026 |
| 0.4.1 | 1 | 6/23/2026 |
| 0.4.0 | 0 | 6/23/2026 |
| 0.3.1 | 0 | 6/23/2026 |
| 0.3.0 | 0 | 6/23/2026 |
| 0.2.1 | 0 | 6/23/2026 |
| 0.2.0 | 1 | 6/23/2026 |
| 0.1.0 | 1 | 6/22/2026 |