Generate, lint, and parse OmniPlan .oplx files. Companion to the oplx-format spec.
Project description
oplx-tools
Python tooling for OmniPlan .oplx documents. Generate, lint, and parse .oplx files without OmniPlan running or installed.
Naming note:
oplxis the file extension OmniPlan uses (.oplx). This project is not related to Yamaha OPL audio synthesis chips (OPL2/OPL3/OPL4) which share a similar string in some retro-audio communities.
Ecosystem
This repo is one of three working together:
| Repo | What it is | When to use |
|---|---|---|
| ๐ oplx-format | The file-format specification (CC-BY-4.0) | Read this if you're writing any .oplx tool in any language |
| ๐ oplx-tools (this repo) | Python implementation: generate / lint / parse โ no OmniPlan needed | Headless .oplx workflows: CI/CD, batch generation, ETL |
| ๐ค omniplan-mcp | MCP server for live OmniPlan automation (requires OmniPlan running) | Conversational task management with Claude โ "schedule a task tomorrow at 2pm" |
| ๐ฆ lash | The installer used to wire oplx-tools's lint hook into Claude Code |
One-shot setup for the agent integration |
Picking between live MCP and headless oplx-tools:
- Use omniplan-mcp when you want conversational interaction with the running app (open documents, query/edit live tasks, schedule changes).
- Use this repo when you want to read, create, or edit
.oplxfiles without running OmniPlan at all โ agents on Linux, CI pipelines, batch generation from spreadsheets, headless tests, mass migration, etc. No GUI, no license, no Mac required.
Prior art
- liyanage/omniplan-python โ older Python library focused on read-side data access. Different scope from
oplx-tools(this repo emphasizes from-scratch file generation, lint, and silent-corruption detection). Use both if you need both reading and writing.
PRs that improve interop with existing libraries are welcome.
What's here
oplx generateโ build a minimum-viable.oplxfrom a YAML/JSON description (no OmniPlan required)oplx lintโ validate a.oplx(zip or directory) against the format spec; catch silent-corruption patterns before they biteoplx parseโ extract tasks/resources/dependencies/assignments from an.oplxfor downstream tools (BI, ETL, integrations)
The spec lives in a separate repo (oplx-format) so it can be referenced by tool authors in any language. This repo is the Python reference implementation.
Use cases
These all work without OmniPlan running or installed:
1. Generate .oplx files from external sources
Drive OmniPlan's task tree from your existing data. Turn a CSV, JSON dataset, or YAML manifest into a valid .oplx your stakeholders can open in OmniPlan to review:
oplx generate roadmap.yaml --out roadmap.oplx
Useful for: project bootstrapping from templates, generating per-customer plans, converting Jira/Linear/GitHub issue exports into a Gantt chart your PMs can review and edit, weekly auto-regenerated baselines.
2. Headless lint in CI/CD
Run on Linux runners with no Mac, no OmniPlan license:
# .github/workflows/oplx-lint.yml
- run: uv tool install oplx-tools
- run: oplx lint plans/*.oplx # CRITICAL findings exit non-zero, fail the build
Catches silent-corruption patterns the moment they're committed (uppercase <type>, lowercase kind=, units="0", orphaned tasks not reachable from <top-task>). See silent-corruption catalog.
3. Bulk-edit existing files without touching OmniPlan
Read in, mutate, write out:
from oplx import parse, generate
from oplx.models import TaskType
doc = parse("project.oplx")
for task in doc.actual.tasks:
if "[milestone]" in task.title:
task.type = TaskType.MILESTONE
task.title = task.title.replace("[milestone]", "").strip()
generate(doc, "project-fixed.oplx")
Works on a stack of files in a loop. No GUI thrash, no per-file dialog acceptance.
4. Parse for downstream BI / ETL
OmniPlan's CSV export is lossy (no dependency kinds, no constraint dates, no custom data types). The XML form preserves everything:
from oplx import parse
doc = parse("project.oplx")
total_effort = sum(t.effort or 0 for t in doc.actual.tasks)
critical_path = [t for t in doc.actual.tasks if t.total_slack == 0]
Pipe into your warehouse, dashboard, or Slack digest.
5. Generate baselines programmatically
The OmniPlan UI's "Set Baseline" button has no API equivalent in OmniPlan's AppleScript dictionary or Omni Automation surface. oplx generate (with the Project.baselines= field) creates a multi-scenario doc directly โ useful for capturing weekly snapshots automatically without UI clicking.
6. Agent-driven workflows
With the Claude Code lint hook installed, agents can read/create/edit .oplx files freely; any silent-corruption pattern they introduce is immediately surfaced in their next turn so they self-correct. This composes naturally with omniplan-mcp when the live app is also available โ the MCP for runtime queries, this repo for file mutation.
Status
- Verified against: OmniPlan 4.10.2
- Spec version: 0.1.0
- Maturity: alpha โ works for the cases tested in the research repo (cycles 0011โ0023). Not yet battle-tested in production.
- Python: 3.11+
Install
Quick install (CLI only)
Homebrew (macOS):
brew tap johntrandall/tap
brew install oplx-tools
uv (Python tool, cross-platform):
uv tool install oplx-tools
# Or from a local clone:
uv tool install ~/dev/oplx-tools
The CLI command is oplx. The full install path below adds the Claude Code lint hook on top of the CLI.
Full install (CLI + Claude Code lint hook)
If you're using Claude Code (or any tool with the same PostToolUse hook surface) and you want agents to be auto-corrected the moment they introduce a silent-corruption pattern, install via lash โ a tiny manifest-driven installer.
lash install reads the lash.json manifest in this repo and:
- Installs the
oplxCLI (uv tool install). - Symlinks
hooks/oplx-lint.shโ~/.claude/hooks/oplx-lint.sh. - Patches
~/.claude/settings.jsonwith aPostToolUseEdit|Write|MultiEditblock that runs the hook.
# One-time prerequisite โ zero-dep installer (single Python script):
uv tool install lash-installer
# Or from a clone: uv tool install git+https://github.com/johntrandall/lash
# Clone this repo and install:
git clone https://github.com/johntrandall/oplx-tools ~/dev/oplx-tools
cd ~/dev/oplx-tools
lash install # runs all 3 operations above
After install, restart Claude Code (or open /hooks once) so the settings watcher picks up the new entry.
lash install is idempotent โ re-run safely after git pull to refresh. See lash README for full manifest semantics.
lash status # see what's installed
lash uninstall # reverse all operations cleanly
What the hook does
The oplx-lint.sh hook fires after Edit/Write/MultiEdit when the file path resolves to inside an .oplx directory bundle. It runs oplx lint <bundle> on that bundle and emits any findings to stderr โ non-blocking (exits 0 even on findings; the agent sees them in the next turn and decides whether to self-correct).
Severity tiers (from silent-corruption.md):
- CRITICAL โ file-level rejection (e.g.,
<type>MILESTONE</type>uppercase silently makes OmniPlan refuse to open the doc) - HIGH โ content silently dropped on save (e.g., orphaned tasks,
units="0"deleting an assignment) - MEDIUM โ value normalized to default (e.g.,
<recalculate>none</recalculate>โduration) - LOW โ cosmetic / metadata
To disable the hook without uninstalling the CLI: lash uninstall && uv tool install oplx-tools.
Quick examples
Generate a minimal doc
cat > project.yaml <<'EOF'
title: My Project
start_date: 2026-06-01T13:00:00Z
tasks:
- id: t1
title: Plan
effort: 14400 # 4 hours
- id: t2
title: Build
effort: 28800
depends_on: [t1]
- id: t3
title: Ship
type: milestone
depends_on: [t2]
EOF
oplx generate project.yaml --out project.oplx
open -a OmniPlan project.oplx
Lint an existing doc
oplx lint project.oplx
# Checks for: orphaned tasks, uppercase <type>, lowercase kind=, units="0",
# unreachable child-task chains, etc.
Parse for downstream use
oplx parse project.oplx --format json | jq '.tasks[].title'
from oplx import parse
doc = parse("project.oplx")
for task in doc.actual.tasks:
print(task.title, task.effort, [d.idref for d in task.prerequisites])
Project layout
src/oplx/
โโโ __init__.py
โโโ cli.py # Entry point: `oplx` command
โโโ models.py # Dataclasses for Project, Scenario, Task, Resource, Dependency
โโโ generate.py # YAML/dict โ .oplx zip
โโโ lint.py # .oplx โ list of warnings/errors
โโโ parse.py # .oplx โ models
โโโ xml/ # Element-by-element serializers/deserializers
Coverage vs spec
oplx generate:
- โ All 4 task types (with hammock via XML hand-write since omniJS rejects)
- โ All 4 dependency kinds with optional lead-time (duration or percentage)
- โ All 4 date constraints
- โ Multi-resource assignment with units
- โ Custom data (string values; other types not yet wired)
- โ Per-resource schedule overrides (basic)
- โ Multi-baseline scenarios
- โ Zip variant output (default) or directory bundle
- โ ๏ธ 3-pt estimation: emit
<min/expected/max-estimate>and let OmniPlan PERT-compute<effort> - โ Note rich-text formatting (bold/color/alignment) โ plain text only
- โ
<filter>saved-filter generation (would need NSPredicate bplist construction)
oplx lint:
- โ Silent-corruption catalog (all CRITICAL + HIGH tier patterns)
- โ Element-ordering rules
- โ
Element-presence requirements (every task reachable from
t-1) - โ ๏ธ XML schema validation (loose; relies on examples not a formal XSD/Relax-NG)
oplx parse:
- โ Reads zip variant and directory bundle
- โ Resolves dependencies, assignments, schedules
- โ Handles multi-scenario docs (Actual + baselines)
- โ ๏ธ Note rich-text: returns concatenated
<lit>text; structure preserved as raw XML if requested - โ Filter bplist decode (use
plistlibdirectly if needed)
License
MIT (see LICENSE). The format itself is described in the oplx-format repo under CC-BY-4.0.
Contributing
This is alpha software. Expected places needing work:
src/oplx/xml/โ more thorough element handling (each element gets its own ser/de)tests/โ round-trip tests againstoplx-formatexamplesoplx lintโ additional silent-corruption patterns as they emerge- Note rich-text formatting (bold/color/alignment) โ currently plain-only
PRs welcome. Please include a fixture (tests/fixtures/) demonstrating the case.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file oplx_tools-0.1.1.tar.gz.
File metadata
- Download URL: oplx_tools-0.1.1.tar.gz
- Upload date:
- Size: 22.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
653fff4395fde8ac3a740f0081ddbf5ff30c89d473df0f27dc558b13f0046d7c
|
|
| MD5 |
0d571a7b8f4490e9b594b23535e95626
|
|
| BLAKE2b-256 |
72500e78f3f129002032c6739d3d576ee4dddba537c86657b3900e97834cb85d
|
File details
Details for the file oplx_tools-0.1.1-py3-none-any.whl.
File metadata
- Download URL: oplx_tools-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5f44f16ada8d9e9eedfe3f07c58e8f064ed763233d66ce69c4d6b6f0743a07d
|
|
| MD5 |
a1923d1000ecdd94e3982418a61f15a8
|
|
| BLAKE2b-256 |
fda2f65cdecd14afb56042b992565684b5ccdfd2d6857901a21e6f937d84c029
|