Skip to main content

Agent-ready command-line interface for OpenProject (work packages, projects, time, costs, search, and more). Installs the `openproject` command.

Project description

openproject — the agent-ready OpenProject CLI

A fast, scriptable OpenProject CLI for managing work packages, projects, time tracking, comments, and per-person cost reports from your terminal — and the first OpenProject command-line tool designed to be driven by AI agents (Claude, Cursor, LLM tool-loops) as well as humans.

PyPI CI Python License: MIT Agent ready

Install: pipx install agent-tool-openproject-cli — then run openproject guide.

agent-tool-openproject-cli (the installed command is openproject) is a Python command-line interface for the OpenProject REST API v3. It covers the whole day-to-day workflow — work packages (create/update/delete/move), assignees, custom fields, projects, comments, time entries, a genuinely good work-package search, notifications, wiki, attachments with Nextcloud file links, and per-person time & cost reporting for monthly invoicing — all with first-class JSON output so it slots straight into automation and AI agents.

Why this OpenProject CLI?

  • 🤖 Agent-ready — structured JSON on stdout, structured errors on stderr, and stable exit codes. Plus a built-in openproject guide playbook so an agent can learn the tool from the tool itself. See AGENTS.md.
  • 🔎 Discoverable search — filter with plain flags (--mine, --overdue, --updated-since 7d), one-word presets (search mine), or --where expressions. Never memorise filter JSON: search fields, search operators, search values.
  • 🖇️ Four output formatsjson (default), table, markdown, and csv; pick per command with -f, set a default, select exact --fields, or --stream NDJSON for big result sets.
  • 🧪 Safe by default--dry-run previews any write; the client retries transient failures; and openproject context gives sticky per-session defaults.
  • 💶 Invoicing extras — per-person cost reports, and a detailed CSV export that includes time-entry custom fields (which OpenProject's own reports can't).
  • 🔐 Safe credentials — API token stored in the OS keyring (Secret Service / macOS Keychain / Windows Credential Locker), with a 0600 file fallback.
  • 🧰 Escape hatchopenproject raw calls any endpoint the typed commands don't wrap.
  • 📦 Install anywhere — pipx, pip, or a single self-contained binary (no Python required on the target).

Docs: Usage guide · Full command reference · Agent guide

Keywords: OpenProject CLI, OpenProject command line, OpenProject API client, work package automation, time tracking CLI, project management CLI, AI agent tool, LLM tooling, Claude, DevOps automation, invoicing.

Compatibility

  • OpenProject API: the stable REST API v3 (HAL+JSON).
  • Tested against: OpenProject 13, 14, 15, 16, and 17 (Community, all-in-one). The full integration suite passes on every one — the CLI covers the whole feature set across all five majors. (15.x also runs in CI on every push; the compat workflow re-runs the 13–17 matrix.)
  • Version notes uncovered by testing:
    • v16+ — time entries require a work package. Logging time against a project only (time add --project with no --work-package) is rejected with "Logged for can't be blank"; log against a work package instead.
    • v17 — the Docker image needs a real SECRET_KEY_BASE env var to boot (a container-config change, unrelated to the CLI).
    • The time-entry work-package filter name differs across versions (work_package_id on ≤15 vs the newer entity_* form); the CLI detects and adapts automatically.
  • Python: 3.10+.

Anything version-specific can be reached directly with openproject raw <method> <path>.


Quick start

1. Install

Pick whichever fits your target:

a) pipx (recommended — isolated, puts openproject on your PATH, needs Python 3.10+)

pipx install agent-tool-openproject-cli          # installs the `openproject` command

b) pip

pip install agent-tool-openproject-cli           # from PyPI
# or, for local development from a clone:
python3 -m venv .venv && . .venv/bin/activate && pip install -e .

c) Single self-contained binary (no Python on the target)

Download the prebuilt binary for your OS from the GitHub Releases page, then:

chmod +x openproject-linux-x86_64 && mv openproject-linux-x86_64 /usr/local/bin/openproject
openproject --help

Or build one yourself — produces a single dist/openproject (~16 MB) that bundles the interpreter and all deps, including the OS keyring backends:

pip install -e '.[build]'
./scripts/build_binary.sh          # -> dist/openproject   (.exe on Windows)

CI (.github/workflows/release.yml) builds Linux/macOS/Windows binaries on every v* tag and attaches them to the release.

The command is openproject. The short name op is intentionally not claimed, to avoid clobbering another tool of that name (this machine already has an OpenProject CLI at /usr/local/bin/op). Add your own alias if you want a shorter command, e.g. alias opr=openproject.

2. Bring up a local OpenProject (for development/testing)

docker compose up -d                 # boots the all-in-one image on :8090
docker compose logs -f openproject   # watch until "seeding" finishes (~5 min)
eval "$(./scripts/get_admin_token.sh)"   # mints & exports APITOKEN

Web UI: http://localhost:8090 — admin / AdminPassw0rd!

3. Log in

openproject auth login --url http://localhost:8090 --token "$APITOKEN"
openproject auth whoami

login verifies the token against /users/me, saves the connection profile to ~/.config/op-cli/config.json, and stores the token in your keyring. Thereafter just run openproject ....

For headless/CI use you can skip the keyring entirely and pass everything via environment variables:

export OPCLI_BASE_URL=http://localhost:8090
export OPCLI_TOKEN=opapi-xxxxxxxx
openproject wp list

Command reference

Run openproject <group> --help for full options. Highlights:

Work packages — openproject wp

openproject wp create "Fix login bug" --project my-proj --type Bug \
    --assignee me --priority High --due-date 2026-08-01 --estimated 4
openproject wp get 42
openproject wp list --project my-proj --status open --assignee me
openproject wp update 42 --status "In progress" --done-ratio 50
openproject wp move 42 other-project
openproject wp assign 42 jane.doe          # or `openproject wp assign 42 none` to clear
openproject wp watch 42 me
openproject wp schema --project my-proj --type Task   # discover fields + custom fields
openproject wp delete 42 -y

Search — openproject search

The star feature, designed to be usable without memorising the JSON filter language. Three ways to search, easiest first:

1. Plain-language flags on search wp:

openproject search wp "payment timeout"                    # full text
openproject search wp --mine --overdue                     # my past-due items
openproject search wp --project my-proj --status open --assignee jane.doe
openproject search wp --type Bug --priority High --updated-since 7d
openproject search wp --unassigned --project my-proj
openproject search wp --version "Sprint 12" --all
openproject search wp --id 42,57,103                       # specific ids
openproject search wp --due-before 2026-08-01              # or --due-before +14d
openproject search wp "invoice" --count                    # just the total
openproject search wp --group-by status --project my-proj

Dates accept ISO (2026-08-01) or relative specs: 7d, 2w, 1m, +30d, today, yesterday.

2. Presets — common searches as one word:

openproject search mine           # open, assigned to me
openproject search overdue        # past due & still open
openproject search unassigned     # open with no assignee
openproject search reported       # I created them
openproject search watching       # I'm watching
openproject search recent --days 3

3. --where expressions — compact, no JSON, repeatable (AND-ed):

openproject search wp --where "status = open" --where "assignee = me"
openproject search wp --where "assignee:none"              # unassigned
openproject search wp --where "updated > 7d" --where "priority = High"
openproject search wp --where "subject ~ timeout"

And when you don't remember what's available, ask the CLI:

openproject search fields                 # what you can filter on (live)
openproject search fields --project x     # incl. that project's custom fields
openproject search operators              # what =, o, !*, <>d, ~ … mean
openproject search values status          # allowed values for a field
openproject search values type
openproject search values version --project x

The raw JSON filter escape hatch is still there when you need it:

openproject search wp --filters '[{"customField1":{"operator":"~","values":["INV-2026"]}}]'

Projects — openproject project

openproject project create "Client X" --identifier client-x --description "..."
openproject project list                       # add --archived for archived ones
openproject project archive client-x           # PATCH active=false
openproject project unarchive client-x
openproject project delete client-x -y

Assignees & members — openproject wp assign, openproject member, openproject user

openproject user available my-proj             # who can be assigned in a project
openproject member add --project my-proj --user jane.doe --role Member
openproject member list --project my-proj
openproject member update 12 --role "Project admin"   # replaces the role set
openproject member remove 12 -y

Comments — openproject comment

openproject comment add 42 "Deployed to staging."
openproject comment list 42
openproject comment edit 137 "Deployed to staging (corrected)."   # 137 = activity id

Time entries — openproject time

openproject time add 2.5 --work-package 42 --activity Development --comment "debugging"
openproject time add 1 --project my-proj --date 2026-07-10        # log against a project
openproject time list --user me --month 2026-07
openproject time edit 88 --hours 3
openproject time activities --project my-proj
openproject time delete 88 -y

Hours accept decimals (2.5) or ISO-8601 (PT2H30M) — they're converted for you.

Custom fields — openproject cf

Custom fields can't be created via the API (admin/Rails only), but you can discover them and set values:

openproject cf wp --project my-proj --type Task     # lists customFieldN + allowed values
openproject wp create "Task" --project my-proj \
    --custom-fields '{"customField1":"INV-2026-007","customField2":{"href":"/api/v3/custom_options/3"}}'

Value-type fields (string/int/date/bool) take a scalar; list/user/version fields take {"href": "..."} (use the ids from openproject cf wp).

Time & cost reporting (invoicing) — openproject cost

OpenProject's API exposes no hourly rates or cost reports, so this report sums time entries per person and applies a rate table you supply:

openproject cost report --month 2026-07 --rates rates.json
openproject cost report --from 2026-07-01 --to 2026-07-31 --user jane.doe --rates rates.json

rates.json (see rates.example.json):

{
  "currency": "EUR",
  "default": 90,
  "users":    { "admin": 120, "jane.doe": 100 },
  "projects": { "client-x": { "default": 100, "admin": 110 } }
}

Without --rates it reports hours only. Most specific rate wins: project+user → project default → user → global default.

Detailed export with time-entry custom fields — one row per entry, including the time entry's custom fields (something OpenProject's own reports can't export). Perfect for invoicing spreadsheets:

openproject cost report --month 2026-07 --rates rates.json --detailed -o csv > july.csv
# columns: date,user,project,workPackage,activity,hours,rate,amount,comment,Cost Center,Billable,…

Set the custom fields when logging: openproject time add 2 -w 42 --custom-fields '{"customField3":"CC-1000","customField4":true}' (discover them with openproject cf time).

Notifications — openproject notify

openproject notify list                # unread by default; --state read|all
openproject notify list --today        # only today's
openproject notify count               # {"total": N, "unread": M}
openproject notify count --today       # adds today / todayUnread
openproject notify read 5
openproject notify unread 5
openproject notify read-all

Attachments — openproject attach

openproject attach upload 42 ./report.pdf --description "Q3 report"
openproject attach list 42
openproject attach download 7 -O ./out.pdf
openproject attach delete 7 -y

Nextcloud / file storages — openproject filelink

Links an existing file in a configured storage (e.g. your Nextcloud) to a work package. The storage must be set up in OpenProject admin first.

openproject filelink storages
openproject filelink add 42 --storage 1 --file-id 123 --file-name "Contract.pdf"
openproject filelink list 42
openproject filelink delete 9 -y

Wiki — openproject wiki

Limitation: OpenProject API v3 only exposes wiki metadata (id + title) and page attachments — there is no API to read or write the wiki body text.

openproject wiki get 3
openproject wiki attachments 3

Raw escape hatch — openproject raw

openproject raw get work_packages/42
openproject raw get work_packages -p 'filters=[{"status":{"operator":"o","values":null}}]'
openproject raw post work_packages -d '{"subject":"x","_links":{"project":{"href":"/api/v3/projects/1"},"type":{"href":"/api/v3/types/1"}}}'
openproject raw patch work_packages/42 -d '{"lockVersion":3,"subject":"y"}'
openproject raw delete work_packages/42

Auth & profiles — openproject auth

openproject auth login --url https://op.example.com --token opapi-xxxx --name prod
openproject auth status
openproject -p prod wp list          # use a specific profile per-invocation
openproject auth logout --name prod

Output format & settings — openproject settings

Four output formats: json (default), table, markdown, and csv. Choose per command, or set a persistent default.

openproject wp list -o table                 # global flag (before the command)
openproject wp list --format markdown        # --format/-f works AFTER any command too
openproject search wp --mine -o csv          # spreadsheet export
openproject wp get 42 --fields id,subject,assignee.name    # pick exact fields (dotted ok)

openproject settings set-format table        # persist a default
openproject settings show                    # current default, config path, profiles

Preview & scale flags (work anywhere on the line):

openproject wp update 42 --status Closed --dry-run   # prints the request, sends nothing
openproject search wp --project big --stream          # NDJSON, one object per line, as fetched

The client also retries transient failures (429 rate-limits + 5xx on reads) with backoff, honoring Retry-After.

Session context — openproject context

Stop repeating --project/--user/filters: set them once and they apply to later commands as defaults (explicit flags always win; --no-context bypasses).

openproject context set --project webshop --assignee me
openproject context show               # see the active defaults
openproject wp list                    # behaves like: wp list --project webshop --assignee me
openproject wp list --project other    # explicit flag wins
openproject --no-context wp list       # ignore the context for one command

openproject context save sprint        # name it, switch between saved contexts
openproject context use sprint

On the first interactive run the CLI asks which default format you'd like and saves your choice. Non-interactive runs (pipes, CI, agents) never prompt — they default to json. Precedence: --format/-f-o/--output$OPCLI_FORMAT → saved default → json.


Output for agents

  • Default output is pretty JSON on stdout. Errors are structured JSON on stderr with a non-zero exit code ({"error": "...", "status": 404}).
  • Exit codes: 0 ok, 4 auth, 5 not found, 6 conflict, 7 validation, 1/3 general/config.
  • Reference-by-name works everywhere sensible: --status "In progress", --assignee jane.doe, --project client-x, --activity Development, --type Bug are resolved to ids for you. me resolves to the current user.
  • Discover filters at runtime with openproject search fields, search operators, and search values <field> — no need to memorise the filter JSON.

Testing

A full integration suite drives the real CLI against the live instance.

docker compose up -d
eval "$(./scripts/get_admin_token.sh)"
export OPCLI_BASE_URL=http://localhost:8090 OPCLI_TOKEN="$APITOKEN"
./scripts/seed_test_data.sh          # modules + custom fields + a 2nd user
pip install -e '.[test]'
pytest                               # 56 tests

A few tests need a second, non-admin actor (to generate a notification). Set OPCLI_SECOND_TOKEN to jane.doe's token to enable them:

export OPCLI_SECOND_TOKEN=$(./scripts/get_admin_token.sh jane.doe | grep APITOKEN | cut -d= -f2)

Without OPCLI_BASE_URL/OPCLI_TOKEN the integration tests skip and only the pure-unit tests run — handy for CI. See docs/API_NOTES.md for the researched API details behind the implementation.

Or just use the Makefile:

make up        # docker compose up + wait for health
make seed      # mint token, seed test data, write .env
make test      # run the suite
make down      # tear down

Security notes

  • The API token is the only secret persisted, and it goes to the OS keyring by default. openproject auth status shows which backend is in use.
  • On a headless box with no Secret Service, the token falls back to a 0600 file at ~/.config/op-cli/credentials.json and the CLI warns you.
  • OPCLI_TOKEN in the environment always takes precedence (nothing is written to disk in that mode).

Known limitations (API v3, not the CLI)

  • Wiki is read-only metadata (no body text over the API).
  • Costs: no hourly rates or cost-report endpoints exist — hence the client-side rate table for invoicing.
  • Custom fields can't be created via the API (admin UI / Rails only); the CLI discovers and sets them.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_tool_openproject_cli-0.3.0.tar.gz (79.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_tool_openproject_cli-0.3.0-py3-none-any.whl (78.3 kB view details)

Uploaded Python 3

File details

Details for the file agent_tool_openproject_cli-0.3.0.tar.gz.

File metadata

File hashes

Hashes for agent_tool_openproject_cli-0.3.0.tar.gz
Algorithm Hash digest
SHA256 ceceb86fa6f96ec9ebfe1f2b29d2da2882ed288a2d7d010c8ca8ebc7b3484d6c
MD5 be7016eceb3fe4ba64941875d2439df7
BLAKE2b-256 e2e5963833c949a84ba10e7fa7aece6f6272970b1f84b92a5bf7f205bd1d268e

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_tool_openproject_cli-0.3.0.tar.gz:

Publisher: release.yml on alexander-zierhut/agent-tool-openproject-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_tool_openproject_cli-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_tool_openproject_cli-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cb0c9a22fb61838fb284bd31efa135ba904900e656028354df1986b76891cd88
MD5 51425b6bcf41129649885c3976f1e388
BLAKE2b-256 a718380239e194b178cb70211cabf3b2d207ad5b795d295e84f3127e62453d40

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_tool_openproject_cli-0.3.0-py3-none-any.whl:

Publisher: release.yml on alexander-zierhut/agent-tool-openproject-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page