9.1 KiB
name, description
| name | description |
|---|---|
| openproject | Interact with the user's self-hosted OpenProject instance over its REST API v3. Use whenever the user wants to view, search, create, update, comment on, close, delete, or report on OpenProject projects, work packages, tasks, bugs, features, milestones, tickets, or time entries — e.g. "what's assigned to me in OpenProject", "create a task in the Infra project", "move |
OpenProject
Read and manage the user's self-hosted OpenProject instance through its API v3.
The server URL and API token are configured in config.json at the plugin root
(baked for this single-user, local-network install). Everything runs through one
stdlib-only Python CLI — no pip installs, no external dependencies, works on
macOS/Linux/Windows with just a Python 3 interpreter.
The tool
All logic lives in ${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py.
Invoke it with python3 (use python or py -3 on Windows if python3 isn't on PATH):
OP="${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py"
python3 "$OP" <command> [options]
If ${CLAUDE_PLUGIN_ROOT} is not set in your shell, use the absolute path to this
skill's scripts/op.py. Every command accepts --json for raw HAL+JSON output
(prefer this when you need exact field values to chain into another call; otherwise
the default human-readable tables are easier to read back to the user).
Always run ping first in a session if you're unsure the token is valid — it
fails fast with a clear message instead of a confusing error mid-task.
Command reference
| Goal | Command |
|---|---|
| Verify connection + auth | op.py ping |
| Who am I | op.py me |
| List / search projects | op.py projects [--search TEXT] |
| List / search work packages | op.py wp list [--project P] [--assignee me] [--status open|closed|NAME] [--type T] [--query TEXT] [--sort field:asc] [--limit N] |
| Show one work package | op.py wp show ID |
| Create a work package | op.py wp create --project P --subject "..." [--type T] [--description "..."] [--assignee U] [--status S] [--priority P] [--parent ID] [--start YYYY-MM-DD] [--due YYYY-MM-DD] |
| Update a work package | op.py wp update ID [--subject] [--status] [--assignee] [--description] [--priority] [--done-ratio N] [--start] [--due] [--parent] |
| Add a comment | op.py wp comment ID --text "..." |
| Change status / close | op.py wp close ID [--status-name "Closed"] |
| Delete (guarded) | op.py wp delete ID --yes |
| Log time | op.py time log --wp ID --hours 1.5 [--comment "..."] [--date YYYY-MM-DD] [--activity NAME] |
| Relate two work packages | op.py wp relate ID --to ID --type blocks|precedes|relates|requires|... |
| Create a project | op.py project create --name "..." [--identifier slug] [--description] [--parent REF] |
| Add a project member | op.py members add --project P --user U --role "Member" |
| Import a spreadsheet | op.py import xlsx FILE.xlsx --sheet "Sheet" --project-name "..." [--execute] |
| List users / roles | op.py users [--search TEXT] · op.py roles |
| List types / statuses / priorities | op.py types [--project P] · op.py statuses · op.py priorities |
| Anything else (escape hatch) | op.py raw GET|POST|PATCH|DELETE /api/v3/... [--data '{...}'] |
How to work with this
- Friendly references resolve automatically.
--project,--assignee,--type,--status,--priorityaccept either a numeric id or a name (e.g.--project "Infrastructure",--assignee me,--status "In progress",--priority High). The CLI looks up the id for you.--assignee meand--assignee none(unassign) are special. - Updates are concurrency-safe.
wp updateandwp closefetch the currentlockVersionautomatically before PATCHing, so you never need to manage it. If an update fails with a lockVersion error, the record changed underneath you — just retry. - Status filtering shortcuts:
--status openand--status closeduse the API's open/closed operators; any other value is treated as a status name/id. - Prefer the typed subcommands over
raw. Reach forrawonly for endpoints the CLI doesn't wrap (versions, categories, queries, attachments, notifications, etc.) — seereference/api-map.mdfor the endpoint catalog andreference/filters.mdfor the filter JSON syntax.
Importing a spreadsheet into a project
op.py import xlsx reads an .xlsx (via the bundled stdlib xlsx_read.py — no
openpyxl) and builds a project: one parent Summary task per Category, with each
row as a child Task. It is a dry run by default — it prints the plan (row count,
category hierarchy, owner matches, value mappings, a sample) and writes nothing until
you add --execute. Always run the dry run first and show the plan to the user.
OP="${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py"
# 1) Preview
python3 "$OP" import xlsx "Plan.xlsx" --sheet "To-Do List" --project-name "My Project"
# 2) Commit
python3 "$OP" import xlsx "Plan.xlsx" --sheet "To-Do List" --project-name "My Project" \
--identifier my-project --execute
- Column detection is automatic and case-insensitive by prefix: a Task/Title/Name
column → subject; Category/Group → parent; Owner/Assignee → assignee; plus
Priority, Status, Due, Notes/Dependencies, and
#/Id. Notes, due-date text, owner, and source row # are preserved in each task's description. - Value mapping: priority H/M/L → High/Normal/Low; status Open/In Progress/ Complete → New/In progress/Closed (unknown → New). Setting a non-default status on create is handled with an automatic create-then-patch fallback.
- Owners: matched to existing users when possible; otherwise the name is recorded in the description (no failed rows). People aren't auto-created (no emails to do so).
- Blockers/dependencies are only linked as real relations if the sheet has a
column referencing other rows. Free-text dependencies stay in the description —
use
op.py wp relateto wire them up explicitly afterward. --merge-category "Old=New"(repeatable) merges near-duplicate category names;--limit Nimports only the first N rows (good for a trial run);--task-typeoverrides the child type (defaultTask).
Guardrails (full CRUD, with confirmation on destructive actions)
- Deletes require
--yesand the CLI refuses otherwise. Before deleting anything, confirm with the user in plain language (show whatwp show IDreturns first), then run with--yes. The same caution applies to bulk changes and anyraw DELETE. - Show before you change. For an update/close/delete on a specific item, run
wp show IDfirst so you (and the user) see the current state. - Never print the API token. It lives only in
config.json. Don't echo it, don't paste it into a work package, comment, or any external surface. - When a write succeeds, report the resulting id/status and the web URL
(
<server>/work_packages/<id>) so the user can click through.
Common recipes
OP="${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py"
# What's on my plate (open items assigned to me)
python3 "$OP" wp list --assignee me --status open
# Open bugs in a project, newest first
python3 "$OP" wp list --project "Infrastructure" --type Bug --status open --sort id:desc
# File a task
python3 "$OP" wp create --project "Infrastructure" --type Task \
--subject "Rotate TLS certs" --description "Expires next month." \
--assignee me --priority High --due 2026-07-15
# Progress an item and log time against it
python3 "$OP" wp update 1234 --status "In progress" --done-ratio 40
python3 "$OP" wp comment 1234 --text "Halfway — blocked on DNS."
python3 "$OP" time log --wp 1234 --hours 1.5 --comment "cert work"
# Inspect, then close
python3 "$OP" wp show 1234
python3 "$OP" wp close 1234
Troubleshooting
error: No API key configured/HTTP 401 Unauthenticated→ the token inconfig.jsonis missing/placeholder/expired. Ask the user for a fresh token from OpenProject → My Account → Access tokens → API, and write it into theapi_keyfield of the plugin'sconfig.json(or setOPENPROJECT_API_KEYin the env, which overrides the file).Could not reach <url>→ the server is down or off-network. Confirm the URL inconfig.json(OPENPROJECT_URLoverrides it) and that the machine can reach it.HTTP 422on create/update → a required field or invalid link. The error message echoes the API's validation detail; re-check type/status/priority names exist for that project (op.py types --project P,op.py statuses).HTTP 422: The chosen user is not allowed to be 'Assignee'→ that user isn't a member of the project (with an assignable role). Add them in OpenProject first, or create the work package without--assignee.