# oa — Office Automation CLI Reference
Windows-only CLI tool that automates Microsoft Office (PowerPoint + Excel) via COM.
## Quick Start
```bash
# Update a single presentation with new Excel data
oa update report.pptx -e data.xlsx
# Batch process 26 countries from a runfile
oa run batch.toml
# Validate all outputs against Excel
oa check batch.toml
# Inspect a PPTX file (read-only)
oa info report.pptx
# Per-slide shape breakdown
oa info -v report.pptx
# Search text inside a PPTX (no PowerPoint needed; exit 1 = not found)
oa find report.pptx -t "[country]"
# Show all config keys and defaults
oa config
# Kill zombie Office processes
oa clean
```
---
## Commands
### `oa update` — Run the update pipeline
The main command. Processes PPTX files by re-linking OLE objects, populating tables, swapping delta indicators, applying color coding, and updating charts.
```
oa update <FILES...> [OPTIONS]
```
**Arguments:**
| `<FILES>` | One or more PPTX files (glob patterns like `*.pptx` supported) |
**Options:**
| `-e, --excel <PATH>` | Excel data file. Auto-detected from OLE links if omitted |
| `-p, --pick` | Open native file dialog to select Excel file |
| `--pair <PPT=XLSX>` | Explicit PPTX=XLSX pair (repeatable) |
| `-o, --output <PATH>` | Output file or directory. Default: in-place |
| `--steps <STEP,...>` | Run only these steps (comma-separated) |
| `--skip <STEP,...>` | Skip these steps (mutually exclusive with --steps) |
| `--set <KEY=VALUE>` | Override a config value (repeatable) |
| `-r, --replace <FIND=VALUE>` | Replace a literal text token everywhere (repeatable, case-sensitive; slides, masters, layouts). Spaces around `=` are ignored when quoted |
| `--check` | Run validation against Excel after processing |
| `--dry-run` | Show what would happen without saving |
| `-v, --verbose` | Enable debug logging |
| `-q, --quiet` | Suppress all output except errors |
**Pipeline Steps** (executed in this order):
| `links` | Yes | Re-point OLE links to new Excel file |
| `tables` | Yes | Populate PPT tables from Excel ranges |
| `deltas` | Yes | Swap delta indicator arrows based on sign |
| `coloring` | No | Apply sign-based color coding (_ccst shapes) |
| `charts` | Yes | Update chart data links |
| `replace` | No | Replace literal text tokens given with `-r` / runfile `[replace]`; skipped silently when none are given |
**Pre-pipeline ZIP operations** (run before COM, no PowerPoint needed):
| ZIP pre-relink | Rewrite OLE/chart paths in PPTX XML (0.1s vs 100s via COM) |
| ZIP chart pre-update | Rewrite every chart cache directly in XML: values, category labels, series names, scatter/bubble data (parity with PowerPoint's refresh; links stay manual). Charts with multi-level categories or cell-driven data labels are refreshed by PowerPoint instead |
**Examples:**
```bash
# Basic: update template with new data
oa update template.pptx -e quarterly_data.xlsx
# Save to output directory (original unchanged)
oa update template.pptx -e data.xlsx -o output/report.pptx
# Only update tables and charts (skip links, deltas, coloring)
oa update report.pptx -e data.xlsx --steps tables,charts
# Skip chart updates (everything else runs)
oa update report.pptx -e data.xlsx --skip charts
# Update multiple files with the same Excel
oa update "reports/*.pptx" -e data.xlsx
# Explicit pairs (different Excel per PPTX)
oa update --pair us_report.pptx=us_data.xlsx --pair mx_report.pptx=mx_data.xlsx
# Override config values
oa update report.pptx -e data.xlsx --set ccst.positive_color=#00FF00
# Delta dead bands (decimal: 0.02 = 2 points). Tokens match a whole word of the OLE name,
# so globalnet covers globalnet_pet / globalnet_dig, and delt_ vs delt2_ makes no difference.
oa update report.pptx -e data.xlsx --set delta.threshold.globalnet=0.02 --set delta.threshold.marketnet=0.05
# Global dead band for every delta, with one category overridden
oa update report.pptx -e data.xlsx --set delta.threshold=0.02 --set delta.threshold.marketnet=0.05
# Replace literal text tokens after all other steps (-r is repeatable; --replace is the long form).
# Covers slides, slide masters and layouts; a token found nowhere prints a warning.
oa update report.pptx -e japan.xlsx -r [country]=Japan -r "[wave]=Wave 3"
# Dry run: see what would happen without saving
oa update report.pptx -e data.xlsx --dry-run
# Update and validate results
oa update report.pptx -e data.xlsx --check
# Auto-detect Excel from OLE links in the PPTX
oa update report.pptx
# Open file dialog to select Excel
oa update report.pptx --pick
```
---
### `oa run` — Execute a TOML runfile
Batch processing from a TOML configuration file. Processes all jobs sequentially using a single shared COM session (avoids 0x80010001 errors from rapid COM create/destroy).
Prints a rich summary table after all jobs complete showing per-job pass/fail, object counts, timing, and totals.
```
oa run <RUNFILE.toml> [OPTIONS]
```
**Options:**
| `--check` | Run validation after each job |
| `--dry-run` | Don't save changes |
| `-v, --verbose` | Debug logging |
| `-q, --quiet` | Errors only |
**TOML Runfile Format:**
```toml
# output/{name}.pptx — {name} is replaced with the job key
default_output = "output/{name}.pptx"
# Optional: limit which pipeline steps run (default: all)
steps = ["links", "tables", "deltas", "coloring", "charts", "replace"]
# Optional: config overrides (same keys as --set). Quote the dotted keys —
# an unquoted `ccst.positive_prefix` is parsed by TOML as a nested table and rejected.
[config]
"ccst.positive_prefix" = ""
"links.set_manual" = true
"delta.threshold.globalnet" = 0.02
"delta.threshold.marketnet" = 0.05
# Optional: literal text replacements applied after all other steps (same as -r).
# {name} expands to the job name. A job may override an entry with its own `replace`.
[replace]
"[country]" = "{name}"
"[wave]" = "Wave 3"
# Template aliases, then one [[job]] per output file
[templates]
t1 = "templates/region1_template.pptx"
[[job]]
name = "Japan"
template = "t1"
data = "data/tracking_japan.xlsx"
[[job]]
name = "France"
template = "t1"
data = "data/tracking_france.xlsx"
output = "special/france_report.pptx" # per-job output override
replace = { "[country]" = "France (FR)" } # per-job replacement override
# Legacy map form is still accepted: [jobs."template.pptx"] with
# name = "data.xlsx" or name = { data = "...", output = "...", replace = { ... } }
```
**Examples:**
```bash
# Run all jobs in the runfile
oa run batch.toml
# Dry run: see what would happen
oa run batch.toml --dry-run
# Run and validate each output
oa run batch.toml --check
# Quiet mode for CI
oa run batch.toml -q
```
**Example output:**
```
Runfile: batch.toml (26 jobs)
--- Job 1/26: Argentina ---
▸ template.pptx
← tracking_argentina.xlsx
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
• Relink ······················· 411 0.1s
• Tables ······················· 155 1.2s
• Deltas ······················· 5 0.4s
• Coloring ····················· 5 0.1s
• Charts ······················· 257 0.3s
✓ completed · 577 objects · 5.9s
[... 24 more jobs ...]
═══════════════════════════════════════
Job summary
✓ Argentina ·················· 577 objects 5.9s
✓ Australia ·················· 577 objects 5.7s
✗ Brazil ····················· Excel file not found
...
✓ all jobs complete · 26/26 files · 15262 objects · 2m 36.1s · avg 5.9s/file
```
---
### `oa check` — Validate PPT against Excel
Cell-by-cell comparison of table values, delta sign verification, and chart data validation. Supports both single PPTX files and batch validation via runfiles.
Exit code 0 = pass, 1 = mismatches found.
```
oa check <FILE> [OPTIONS]
```
**Arguments:**
| `<FILE>` | PPTX file or runfile (`.toml`/`.py`) to validate |
**Options:**
| `-e, --excel <PATH>` | Excel to check against (auto-detected if omitted) |
| `--set <KEY=VALUE>` | Override config values (repeatable) |
| `-v, --verbose` | Show per-cell comparison details |
**What it checks:**
- **Pairs**: Every `delt_`/`ntbl_`/`htmp_`/`trns_` shape must have an OLE object on its slide whose name it contains as a whole word; unpaired ones fail with a closest-name hint (`delt2_globalnet_f: no OLE object matches on this slide (closest OLE name: globalnet_g)`). OLE objects that drive no table or delta are only counted (listed with `-v`, yellow ⚠ — fine if standalone)
- **Tables**: Every cell in every linked table compared to its Excel source
- **Transposed tables**: Handles row/col swap correctly
- **_ccst tables**: Applies the same transform (prefix, symbol removal) before comparing
- **Deltas**: Verifies shape sign suffix (_pos/_neg/_none) matches Excel value; a paired delta that still has no suffix is reported as never updated
- **Charts**: Link targets, series counts, series values, category labels and series names (cached vs Excel)
**Examples:**
```bash
# Check a single file against specific Excel
oa check report.pptx -e data.xlsx
# Deltas updated with thresholds must be checked with the SAME --set values,
# otherwise the dead-band deltas are reported as mismatches
oa check report.pptx -e data.xlsx --set delta.threshold.globalnet=0.02 --set delta.threshold.marketnet=0.05
# Auto-detect Excel from OLE links
oa check report.pptx
# Batch check all jobs from a runfile
oa check batch.toml
# Verbose: see every cell comparison
oa check report.pptx -e data.xlsx -v
# Use in CI: exit code 1 on mismatch
**Example output (single file):**
```
▸ report.pptx
← data.xlsx
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
✓ Tables ·············· 234 checked · 0 mismatches PASS
✓ Deltas ·············· 5 checked · 0 mismatches PASS
✓ Charts ·············· 257 checked (546 series) · 0 mismatches PASS
✓ check passed · 785 checked · 0 mismatches · 4.3s
```
**Example output (batch via runfile):**
```
═══════════════════════════════════════
Check summary
✓ Argentina ·································· 785 checked 4.5s
✗ Australia ·································· 4 mismatches 4.4s
✓ Brazil ····································· 785 checked 4.2s
...
✗ 1 check failed · 5/6 files · 4740 checked · 27.8s
```
---
### `oa info` — Inspect a PPTX file
Read-only inspection. Shows slide count, OLE links, charts (linked/unlinked), special shapes, and delta templates. With `-v`, adds a per-slide shape breakdown table.
```
oa info <FILE> [-v]
```
**Options:**
| `-v, --verbose` | Show per-slide breakdown table |
**Example (normal):**
```bash
oa info template.pptx
```
```
▸ template.pptx
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
File size ···································· 2.7 MB
Slides ······································· 68
OLE links ···································· 155
╰ tracking_data.xlsx ························ 155
Charts ······································· 258
╰ Linked ··································· 257
╰ Unlinked ································· 1
Special shapes ······························· 165
╰ ntbl_ normal tables ······················ 122
╰ htmp_ heatmap tables ····················· 0
╰ trns_ transposed tables ·················· 33
╰ delt_ delta indicators ··················· 5
╰ _ccst color-coded ························ 5
Delta templates
╰ tmpl_delta_pos ··························· ✓
╰ tmpl_delta_neg ··························· ✓
╰ tmpl_delta_none ·························· ✓
```
**Example (verbose — per-slide breakdown):**
```bash
oa info -v template.pptx
```
Appends after the normal output:
```
Per-slide breakdown
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
slide ole chart ntbl htmp trns delt ccst total
1 · · · · · · · ·
2 5 · 5 · · 5 5 20
3 · 19 · · · · · 19
4 7 · 5 · 2 · · 14
...
68 · 19 · · · · · 19
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
68 slides · 42 active · 26 empty
```
Columns: `slide` (slide number), `ole` (OLE objects), `chart` (linked charts only), `ntbl` (normal tables), `htmp` (heatmap tables), `trns` (transposed tables), `delt` (delta indicators), `ccst` (color-coded shapes), `total` (sum). Zero values shown as `·`. All slides shown including empty ones.
---
### `oa diff` — Compare two PPTX files
Side-by-side comparison of two presentations. Read-only, no Excel needed.
```
oa diff <A.pptx> <B.pptx> [-v]
```
**What it compares:**
- Shape inventory counts (ntbl_, htmp_, trns_, delt_, _ccst); numbered delta sets (`delt2_`, ...) get their own count row and `tmpl<N>_delta_*` template rows when present
- Table cell values for matching shapes
- Chart counts
**Examples:**
```bash
# Compare template vs updated version
oa diff template.pptx updated_report.pptx
# Compare two country reports
oa diff us_report.pptx mx_report.pptx
```
---
### `oa find` — Search text inside a PPTX
Read-only, ZIP-level scan (no PowerPoint, no Excel). Lists every occurrence of the given text
with its location, shape name and a snippet. Runs in milliseconds even on a 70-slide deck.
```
oa find <FILE> -t <TEXT> [-t <TEXT>...] [-i]
```
**Options:**
| `-t, --text <TEXT>` | Text to look for, literal (repeatable, required) |
| `-i, --ignore-case` | Case-insensitive matching |
**What it scans:** every slide (in presentation order), each slide's speaker notes, every slide
layout and every slide master. Grouped shapes and table cells are included. Text is joined per
paragraph before matching, so a phrase PowerPoint stored across several formatting runs is
still found. `-t` is the first selector; charts or shapes by name may be added later.
**Examples:**
```bash
# Did the replace step catch every token? (exit 1 means none left)
oa find out/japan.pptx -t "[country]"
# Several needles at once — one section per needle
oa find template.pptx -t "[country]" -t "[wave]"
# Case-insensitive
oa find report.pptx -i -t japan
```
**Example output:**
```
▸ template.pptx
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Slide 1 │ TextBox 3 Market Report: [country]
Slide 3 │ TextBox 60 Geography: [country]
Slide 5 │ TextBox 5 Key findings: [country]
Layout Title Slide │ LayoutFooter Layout footer for [country]
✓ 4 hits for "[country]" · 5 slides · 2 layouts · 2 masters · 3 notes · 0.01s
```
**Exit codes:** `0` at least one hit · `1` no hits · `2` error (file not found, not a PPTX).
---
### `oa config` — Show config keys and defaults
Prints all available `--set` keys with their default values.
```
oa config
```
**Config Sections:**
| `heatmap.*` | 5 keys | Colors for 3-color scale heatmap tables (htmp_) |
| `ccst.*` | 5 keys | Sign-based color coding (_ccst tables) |
| `delta.*` | 5 keys + `delta.threshold.<token>` | Delta template shape names, source slide, and sign dead bands (`delta.threshold` global, `delta.threshold.<token>` per OLE-name token, e.g. `--set delta.threshold.globalnet=0.02`) |
| `links.*` | 1 key | OLE link update behavior |
---
### `oa clean` — Kill zombie Office processes
Finds and kills orphaned POWERPNT.EXE and EXCEL.EXE processes left over from crashes. Shows found processes with PIDs, prompts for confirmation before killing.
```
oa clean [-f]
```
**Options:**
| `-f, --force` | Kill without prompting for confirmation |
**Examples:**
```bash
# Interactive: lists processes and prompts before killing
oa clean
# Force kill (for scripts)
oa clean -f
```
**Example output:**
```
Found 2 Office processes
EXCEL.EXE ························ PID 95448
POWERPNT.EXE ····················· PID 99640
Kill all? [y/N] y
✓ Killed EXCEL.EXE ················· PID 95448
✓ Killed POWERPNT.EXE ·············· PID 99640
✓ cleaned · 2 processes killed
```
When no processes found:
```
Found 0 Office processes
✓ No Office processes found
```
---
## Exit Codes
| 0 | Success |
| 1 | Validation failure (`oa check` found mismatches) or no hits (`oa find`) |
| 2 | Runtime error (bad arguments, missing files, COM failure) |
---
## Special Shape Naming Conventions
The pipeline identifies shapes by name prefix/suffix:
| `ntbl_` | Normal table | Preserves formatting, only updates cell text |
| `htmp_` | Heatmap table | Recalculates 3-color scale from Excel |
| `trns_` | Transposed table | Swaps rows/columns from Excel range |
| `delt_` | Delta indicator | Arrow shape, swapped based on value sign (set 1; `delt1_` is an alias) |
| `delt<N>_` | Delta indicator, set N | Same behaviour, copies from `tmpl<N>_delta_*` (N ≥ 2, e.g. `delt2_Rev_DE_pos`) |
| `_ccst` | Color-coded table | Cells colored by sign (positive/negative/neutral) |
| `tmpl_delta_pos` | Template | Positive delta arrow template on slide 1 |
| `tmpl_delta_neg` | Template | Negative delta arrow template on slide 1 |
| `tmpl_delta_none` | Template | Neutral delta template on slide 1 |
| `tmpl<N>_delta_pos` / `_neg` / `_none` | Template, set N | Templates for `delt<N>_` shapes, same slide as set 1. A set with any template missing is skipped with a warning, never mapped to set 1. |
**Shape-OLE matching:** Table names like `ntbl_Object 1_ccst` are matched to OLE shapes like `Object 1` using word-boundary token matching (the `ntbl_` prefix and `_ccst` suffix are stripped during matching).
**Delta empty data handling:** When the Excel cell for a delta indicator is empty/missing, the delta shape is set to `_none` (neutral indicator) rather than being skipped.
---
## Performance
| Single 68-slide PPTX (155 OLE, 257 charts) | ~6s |
| Batch 26 files via `oa run` | ~2m 36s |
| ZIP pre-relink (411 links) | 0.1s |
| ZIP chart pre-update (257 charts) | 0.3s |
| `oa info` inspection | ~3s |
| `oa find` text search (71-slide deck, ZIP only) | ~0.03s |
| `oa check` single file | ~4s |
| `oa check` batch (6 files via runfile) | ~28s |
| `oa clean` (no processes) | instant |
**Key optimization:** COM session reuse across batch jobs saves ~28s on 26 jobs by avoiding rapid COM create/destroy (GOTCHA #39).
**Limitation:** PowerPoint is a single-instance COM server (GOTCHA #40). Multiple threads all share one POWERPNT.EXE process, so multi-threaded parallelism provides no speedup for PowerPoint operations.