Files
open-project/skills/openproject/SKILL.md
T
2026-06-27 17:25:55 -05:00

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, --priority accept 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 me and --assignee none (unassign) are special.
  • Updates are concurrency-safe. wp update and wp close fetch the current lockVersion automatically 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 open and --status closed use the API's open/closed operators; any other value is treated as a status name/id.
  • Prefer the typed subcommands over raw. Reach for raw only for endpoints the CLI doesn't wrap (versions, categories, queries, attachments, notifications, etc.) — see reference/api-map.md for the endpoint catalog and reference/filters.md for 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 relate to wire them up explicitly afterward.
  • --merge-category "Old=New" (repeatable) merges near-duplicate category names; --limit N imports only the first N rows (good for a trial run); --task-type overrides the child type (default Task).

Guardrails (full CRUD, with confirmation on destructive actions)

  • Deletes require --yes and the CLI refuses otherwise. Before deleting anything, confirm with the user in plain language (show what wp show ID returns first), then run with --yes. The same caution applies to bulk changes and any raw DELETE.
  • Show before you change. For an update/close/delete on a specific item, run wp show ID first 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 in config.json is missing/placeholder/expired. Ask the user for a fresh token from OpenProject → My Account → Access tokens → API, and write it into the api_key field of the plugin's config.json (or set OPENPROJECT_API_KEY in the env, which overrides the file).
  • Could not reach <url> → the server is down or off-network. Confirm the URL in config.json (OPENPROJECT_URL overrides it) and that the machine can reach it.
  • HTTP 422 on 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.