Skip to content

Repository files navigation

Travel Forms Pilot

A conversational helper for German academic business-trip paperwork (Dienstreise), driven by any tool-using LLM. You chat with the LLM; it reads your trip folder, asks the missing questions in one batch, calls a small Python script that fills the official MPIE Word forms, and hands you the signed-ready PDF.

The project is shipped as a skill — a set of plain Markdown instructions plus a couple of Python scripts. It is not a service, not a cloud product, and not specific to any one LLM vendor. It was built and tested with Claude (Cowork / Code) but works with ChatGPT, Gemini, GWDG Chat AI, or any other agent that can read local files and run a shell command.

The default institute is MPI for Sustainable Materials (formerly MPI Eisenforschung, Düsseldorf). Other institutes can fork and adapt one config file plus their own form templates.


What it does, concretely

For each business trip, the pilot manages the whole lifecycle:

  1. A per-trip trip.md (YAML header + free-text sections) that travels with the trip from invitation to reimbursement.
  2. The official Dienstreiseantrag (travel application) DOCX, trimmed to the pages the Reisestelle actually wants (application + A1 for EU trips, or single page for domestic German trips).
  3. The official Reiseabrechnung (expense report) DOCX.
  4. A running ledger of business-earned bonus points (BahnBonus, Miles & More) that have to be reported to the institute.
  5. A check of the travel settlement: when the Reisestelle's settlement letter arrives, the pilot compares what was paid against what you claimed, explains any differences in plain language, and then closes the trip.

Two features work across all your trips:

  • Dashboard — a single self-contained HTML file (dashboard.py) giving an action-first overview of every trip: status, deadlines (Abrechnung, abstract submission, registration / early-bird), missing applications, pending reimbursements, and the type of contribution (invited / plenary / keynote / talk / poster). Portable (pure Python + PyYAML, no internet, no LLM needed to view it), opens in any browser on macOS/Linux/Windows. See Dashboard.
  • Calendar — two entries per trip, confirm-first: an all-day absence block on the shared departmental calendar, and the itinerary (travel legs, hotel stays) on your own calendar, built from the booking confirmations. Runs on the separate calmcp MCP server; see Calendar entries.

Each of (2) and (3) is generated by one Python script call. The LLM's job is to bootstrap a new trip folder, sort the files you dropped in, gather any missing inputs in a single round of questions, show you the YAML config, and call the script. No silent prefilling, no multi-round drip-feed of questions, no manual file sorting.

Workflows (day to day)

The pilot walks a trip through its whole life. In practice you only ever do two things: drop files into the trip folder and talk to the assistant. It keeps everything else in sync — the per-trip trip.md, the official MPIE forms, the calendar entry, and the dashboard. Each step below names the helper it uses; nothing is locked to one LLM or one operating system.

Throughout, whenever a trip.md changes, the assistant re-runs dashboard.py so dashboard.html always reflects the latest state — and you can regenerate it yourself at any time.

A — New business trip

  1. Create a folder named yyyymm_LOCATION_EVENT/ and drop in whatever you have (invitation, programme, links).
  2. Open the assistant in your TRAVEL-FORMS directory and point it at the folder. It scaffolds the folder, sorts your files, fills in trip.md, and asks one batch of questions — transport, cost bearer, A1, document language, the type of contribution (invited / plenary / keynote / talk / poster), the abstract-submission deadline, and the registration / early-bird deadline.
  3. You get a ready-to-sign Dienstreiseantrag PDF. Sign it and email it to the Reisestelle. It then offers to put the absence block on the shared departmental calendar — always asked, never done silently.

Book travel and hotel only after the application is approved — that approval arrives as an update (workflow B).

B — Updating a trip

Something happens — the approval comes back with a trip number, you book a train or flight, you register for the conference. Drop the document into the top of the trip folder and open the assistant on that directory. It sorts the new file and updates trip.md: the trip number and "approved" once the approval arrives, "travel booked" / "hotel booked" on a confirmation, "registered" on a sign-up, and so on. Repeat as often as you like over the life of the trip.

On a travel or hotel booking it also offers the itinerary entries: one timed event per leg and one spanning the hotel stay, on your own ic_travel calendar, with the times, train or flight numbers and addresses read out of the confirmation. Rebook something later and it moves the existing event rather than adding a second one.

C — Checking the dashboard

Open dashboard.html in any browser. It's an at-a-glance, action-first overview of every trip, with alerts for what needs doing — Abrechnung deadlines, missing or unapproved applications, registration deadlines, and trips awaiting reimbursement. It stays current because the assistant regenerates it after every change; if you've been editing trip.md files by hand, run python scripts/dashboard.py <trips-root> once to refresh it. The dashboard is read-only: spot something, edit the trip's trip.md, and it updates.

D — Abrechnung (expense report)

After the trip, drop all your receipts and documents into the trip folder, open the assistant, and answer its questions. It either tells you what's still missing or produces a ready-to-sign Reiseabrechnung PDF. Sign it and send it in (within 3 months — immediately if you took an advance). It also asks about any BahnBonus / Miles & More points to record.

E — Closing the trip

When the administration's settlement letter arrives, drop it into the trip folder. The assistant compares what was paid against what you claimed and explains any differences in plain language (e.g. a per-diem reduced because a meal was provided, or a receipt not reimbursed). If something looks off, it helps you query the Reisestelle; if it's correct, the trip is marked closed. The dashboard then shows only closed trips from the current year, so the list stays focused on what's live (dashboard.py --all-closed shows everything).

The settlement comparison is the one step where the assistant reads two PDFs and reasons about them rather than running a deterministic script — it works with any capable LLM, and it's assistive: you make the final call.

Installation

Five steps. Only the first two are required — 3 and 4 buy you calendar entries and a session that starts already knowing what to do.

1. The repo and its dependencies

You need Python 3.9+, and for PDF output either LibreOffice or Microsoft Word.

git clone https://github.com/biterik/travel-forms-pilot.git
cd travel-forms-pilot

pyyaml is the only hard requirement — see Helper tools immediately below for how to install it against the right interpreter, which is the one thing here that reliably goes wrong.

Helper tools

Two of these are optional, but the first one isn't.

pyyaml — required. Every script reads YAML. Install it for the same python3 you'll actually invoke, which on macOS is usually Homebrew's, not a conda env that only exists inside an activated shell:

which python3                 # e.g. /opt/homebrew/bin/python3
python3 -m pip install pyyaml --break-system-packages

Drop --break-system-packages inside a virtualenv or conda env. If you run the pilot under conda, activate that env before every session — otherwise the assistant's plain python3 won't find pyyaml and will start improvising.

LibreOffice — recommended, for PDF export. This is what turns the filled DOCX into the signable PDF:

brew install --cask libreoffice

If that fails with Connection reset by peer or an SSL error, your network is blocking download.documentfoundation.org — common on institutional networks. Use a mirror instead:

cd ~/Downloads
curl -LO https://ftp.fau.de/tdf/libreoffice/stable/26.2.5/mac/aarch64/LibreOffice_26.2.5_MacOS_aarch64.dmg
hdiutil attach LibreOffice_26.2.5_MacOS_aarch64.dmg
cp -R /Volumes/LibreOffice*/LibreOffice.app /Applications/
hdiutil detach /Volumes/LibreOffice*
xattr -dr com.apple.quarantine /Applications/LibreOffice.app

Check the current version at https://ftp.fau.de/tdf/libreoffice/stable/ and adjust. The xattr line matters: without it the headless launch can hang on a Gatekeeper prompt nobody ever sees, which looks exactly like a silent conversion failure. Note Homebrew won't manage an install done this way — update by re-running the curl.

Microsoft Word — optional fallback. If LibreOffice isn't installed and /Applications/Microsoft Word.app is, the PDF is exported through Word via AppleScript instead. It works and renders the templates faithfully, but it is not headless: Word's window opens, Word stays running, and macOS asks once for permission ("Terminal wants to control Microsoft Word" — allow it, or enable it under System Settings → Privacy & Security → Automation). That's why LibreOffice is tried first.

Converter order and override. soffice first, then Word. To force one:

export TFP_PDF_CONVERTER=soffice   # or: word, auto (default)

With neither installed you still get a correct DOCX and export the PDF yourself — nothing else in the pilot is affected. If a trip folder ever has a DOCX but no PDF, check the converter before suspecting the form logic.

2. Decide where things live

The repo holds code. Three things are yours and stay out of it:

TRAVEL-FORMS/                    ← the folder you connect to your assistant
├── personal/                    ← not in git
│   ├── identity.yaml            ← your personnel number, cost centre, department
│   └── bonus_points.md          ← running BahnBonus / Miles & More balance
├── travel-forms-pilot/          ← this repo
├── 202609_Cargese_MecaNano-school/   ← trip folders, siblings of the repo
└── 202609_ISAM5_Tokyo/

Create your personal files from the shipped examples:

mkdir -p ../personal
cp identity.example.yaml   ../personal/identity.yaml
cp bonus_points.example.md ../personal/bonus_points.md
$EDITOR ../personal/identity.yaml

identity.yaml is the single source of truth for the fields that appear on every form. Fill in name, personnel number, department, cost centre and the institute block. Nothing else needs configuring per trip.

The scripts find it by searching, in order: $TFP_IDENTITY$TFP_PERSONAL_DIR/identity.yaml<trips-root>/personal/ → one level above the repo (the pre-2026 location, still honoured) → ~/.travel-forms-pilot/. So the layout above is a recommendation, not a hard requirement — and moving or re-cloning the repo never orphans your data.

3. Calendar: install the calmcp MCP server

Calendar entries go through a separate MCP server, calendar-mcp-server — a small CalDAV toolkit that keeps passwords in your OS keyring and makes every write a confirm-gated dry-run. The pilot has no calendar credentials of its own and never asks you for a password.

git clone https://github.com/biterik/calendar-mcp-server.git
cd calendar-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[mcp]"

cp calendars.example.yaml calendars.yaml
keyring set calmcp/kerio_personal <your-username>   # prompts, hidden input
calmcp discover                                      # find your real calendars
$EDITOR calendars.yaml                               # paste in the ones you want
calmcp list_calendars                                # check they're reachable

The pilot expects two calendar ids to exist in your calendars.yaml:

id What it's for Role
cm_absence the shared departmental calendar — gets one all-day block per trip writable (owned by someone else)
ic_travel your own calendar — gets travel legs and hotel stays from the bookings owner

Rename them in config/<institute>.yaml (kalender: absenz_calendar_id / reise_calendar_id) if yours are called something else. Then connect the server to your assistant — Claude Desktop via the .mcpb extension bundle, Claude Code via claude mcp add; both are documented in the calendar-mcp-server README.

Skipping this step is fine: the pilot still does everything except calendar entries, and scripts/add_to_calendar.py remains as a CalDAV fallback for the absence block (pip install caldav icalendar).

4. Tell your assistant about the skill

The repo ships a packaged skill in skill/SKILL.md. It is thin by design: it locates the repo on disk and states the rules that must never be broken, then reads everything else — behaviour, heuristics, field tables, trip history — from the repo itself. So it cannot drift out of sync with the scripts, and updating the pilot is just git pull.

Build the bundle and install it:

cd skill && zip -r ../travel-forms-pilot.skill travel-forms-pilot 2>/dev/null || {
  mkdir -p /tmp/skillbuild/travel-forms-pilot
  cp SKILL.md /tmp/skillbuild/travel-forms-pilot/
  (cd /tmp/skillbuild && zip -r travel-forms-pilot.skill travel-forms-pilot)
}

Then save the .skill file into your assistant's skills. In Claude, ask it to package skill/SKILL.md as a skill and save it when offered — after that it is available in every session on every device, and a cloud Cowork task will pick up the pilot without any CLAUDE.md.

See Using with Claude below for how this interacts with CLAUDE.md. For non-Claude assistants see Using with other LLMs.

5. Use it

"New trip: 202609_Cargese_MecaNano-school/ — I dropped the invitation
 and the flight booking in there."

The assistant bootstraps the folder, sorts your dropped files, pre-fills trip.md from the invitation, asks one batched round of questions for what's still open, calls scripts/fill_application.py, hands you the signed-ready PDF, and offers to put the trip in your calendar.

Day-to-day usage

Everything below is said in plain language to the assistant. There are no commands to memorise.

You say What happens
"New trip: 202609_Cargese_MecaNano-school/, invitation is in there" Folder scaffolded, files sorted, trip.md filled, one round of questions, Dienstreiseantrag PDF — then "add to calendar?"
"The approval came back, trip number DR6129" reisenummer + antrag_genehmigt set, dashboard refreshed. Now you may book.
"I booked the ICE and the hotel" Files sorted into 3_Booking/, milestones set, and the travel legs + hotel offered as events on your ic_travel calendar
"I registered / the abstract is in" anmeldung: block updated, deadline alerts clear from the dashboard
"Let's do the expense report" Receipts read, questions asked, Reiseabrechnung PDF — then the bonus-point question
"The settlement letter arrived" Paid vs. claimed compared in plain language, differences explained, trip closed
"Put Tokyo in my calendar" Dry-run first, shown to you, written only on your yes
"How do my trips look?" dashboard.html regenerated — deadlines, gaps, pending reimbursements

Two rules the assistant follows that are worth knowing about:

  • It asks in batches, once per phase — three or four questions in one go, not a drip-feed. Correct any of them in a single reply.
  • It never writes to a calendar without showing you the event first, and it needs a second confirmation for the shared departmental calendar.

Try the scripts directly (no LLM needed)

You can also run the scripts standalone. Example config that mirrors a real MPIE travel application:

# cargese.yaml   (personal fields shown with placeholder values)
output_basename: 20260906_Cargese_Dienstreiseantrag
trim: a1
fields:
  0: Doe                # surname            \
  1: Jane               # first name          |  these five come from
  2: XY                 # department          |  your identity.yaml
  3: W0000000           # cost centre         |
  4: "0000"             # personnel number   /
  6: "MecaNano Summer School, COST CA21121"
  7: "Cargèse, France"
  8: "01.09.2026"
  9: "06.09.2026"
  21: "Miles & More"
  22: "Düsseldorf, 27.05.2026"     # place, date of signature
  26: Doe              # A1: surname
  27: Jane             # A1: first name
  28: "0000"           # A1: personnel number
  29: "01.09.2026"
  30: "06.09.2026"
  31: "Institut d'Études Scientifiques de Cargèse"
  32: "Menasina, 20130 Cargèse"
  33: France
checkboxes: [0, 1, 6, 11, 19]
python scripts/fill_application.py --config cargese.yaml --output-dir ./out

Output:

Template: Dienstreiseantrag_Mitarbeitende_mit_A1.docx  (42 FORMTEXT fields, 21 checkboxes)
Filled 19 FORMTEXT fields.
Checked boxes: [0, 1, 6, 11, 19]
Trimmed to application + A1.
DOCX written: out/20260906_Cargese_Dienstreiseantrag.docx
PDF written:  out/20260906_Cargese_Dienstreiseantrag.pdf

You get a 2-page PDF ready to sign. The full field index table is in docs/formular_mechanik.md.

Repository layout

travel-forms-pilot/
├── README.md                 ← this file
├── LICENSE                   ← MIT
├── SKILL.md                  ← instructions the LLM agent reads first
├── CLAUDE.md.example         ← copy to your workspace root as CLAUDE.md
├── identity.example.yaml     ← copy to personal/identity.yaml and fill in
├── bonus_points.example.md   ← copy to personal/bonus_points.md
├── skill/
│   └── SKILL.md              ← thin installable skill: locates the repo + non-negotiables
├── prompts/
│   ├── 00_pilot.md           ← base behavior: fast mode, batched questions
│   ├── 40_backlog.md         ← backlog mode: import old, already-completed trips
│   ├── 50_dashboard.md       ← dashboard mode: portable overview across all trips
│   ├── 60_calendar.md        ← calendar mode: absence block + itinerary, via calmcp
│   └── 70_closing.md         ← closing mode: check the settlement letter, close the trip
├── scripts/
│   ├── _docx_form.py             ← shared DOCX form-fill engine (stdlib; PDF via Word or soffice)
│   ├── bootstrap_trip.py         ← scaffold a fresh trip folder (subfolders + trip.md)
│   ├── backlog_trip.py           ← import an OLD trip folder (infer status, fill trip.md, sort files)
│   ├── dashboard.py              ← portable overview of all trips → self-contained HTML (+ text)
│   ├── fill_application.py       ← Dienstreiseantrag + A1 in one shot
│   ├── fill_expense.py           ← Reiseabrechnung in one shot
│   └── add_to_calendar.py        ← CalDAV fallback for the absence block (no-MCP sessions)
├── templates/
│   ├── trip.md.tmpl                                  ← per-trip document template
│   ├── Dienstreiseantrag_Mitarbeitende_mit_A1.docx   ← blank application
│   └── Reiseabrechnung_Vorlage.docx                  ← blank expense report
├── config/
│   └── mpi-susmat.yaml       ← institutional constants (mail addresses, rules, EU list)
├── docs/
│   └── formular_mechanik.md  ← field index tables, trim recipes, script usage
└── learnings.md              ← institutional travel memory (grows with every trip)

Personal data stays out of the repo

Three things are yours and are never committed. Each has a tracked *.example.* counterpart in the repo, so a fork gets the shape without anyone's data:

Yours (git-ignored) Ships as What it holds
personal/identity.yaml identity.example.yaml personnel number, department, cost centre, bonus-programme flags, institute block
personal/bonus_points.md bonus_points.example.md running balance of business-earned points awaiting batch reporting
CLAUDE.md at your workspace root CLAUDE.md.example the startup reading order, with your paths

Copy, fill in, done:

mkdir -p ../personal
cp identity.example.yaml   ../personal/identity.yaml
cp bonus_points.example.md ../personal/bonus_points.md
cp CLAUDE.md.example       ../CLAUDE.md

Where identity.yaml may live. The scripts search $TFP_IDENTITY$TFP_PERSONAL_DIR/identity.yaml<trips-root>/personal/ → one level above the repo → ~/.travel-forms-pilot/, and take the first hit. Put it wherever suits your backups; personal/ next to your trip folders is the default because it survives deleting and re-cloning the repo.

Calendar credentials are not in here at all. They live in your OS keyring, managed by the calendar MCP server. identity.yaml only needs a kalender: block if you're on the add_to_calendar.py fallback path, and the example file has it commented out.

Your trip folders (yyyymm_LOCATION_EVENT/) also sit outside the repo. The .gitignore is set up so that if any of this accidentally lands inside the repo, it still won't be committed.

How a trip flows (minimal-work design)

The user does the absolute minimum per trip:

  1. Create a folder named yyyymm_LOCATION_EVENT/ somewhere on disk (e.g. ~/Desktop/Trips/202609_Cargese_MecaNano-school/).
  2. Drop stuff into it — invitations, programme PDFs, train/flight bookings, receipt photos — at the top level. No manual sorting.
  3. Tell the LLM: "New trip, here's the folder."

From there the agent takes over:

  • Runs scripts/bootstrap_trip.py which creates the canonical subfolders (1_Invitation/, 2_Application/, 3_Booking/, receipts/, 5_Expense_Report/, 6_Followup/), copies trip.md.tmpl into the folder as trip.md, and pre-fills the YAML header with the date / location / event extracted from the folder name.
  • Sorts the loose files into the right subfolders (one short table for you to confirm with "ok").
  • Reads the invitation/programme to enrich trip.md (URL, end date, one-line trip purpose, abstract-submission deadline, and the type of contribution inferred from the wording).
  • Asks one batched round of questions for whatever is still open (document language, transport, cost bearer, A1 confirmation, type of contribution, abstract and registration deadlines).
  • Calls scripts/fill_application.py and hands you the signed-ready DOCX + PDF.

After your trip, drop the receipt photos into the same folder and say "let's do the expense report." Same flow with scripts/fill_expense.py. Verify the money receipt and the bonus-point credit afterwards.

Calendar entries

A trip produces two different kinds of calendar entry, and keeping them apart is deliberate:

Absence Itinerary
Calendar cm_absence — shared, departmental ic_travel — yours
Who reads it your colleagues: "Erik is away" you, on your phone, in transit
Content one all-day block over the travel period one event per travel leg, one per hotel stay
When right after the Dienstreiseantrag is generated when the booking confirmations arrive
Built from trip.md the confirmations in 3_Booking/

Booking details never go on the shared calendar — flight numbers and hotel addresses are nobody else's business. The absence block never goes on your personal travel calendar — the itinerary events already cover those days.

How it works

Both go through the calmcp calendar MCP server (installation in step 3 above). The assistant:

  1. Dry-runs the write. calmcp returns a before/after contract and changes nothing.
  2. Shows you the proposed event, with weekdays on the dates.
  3. Writes only after your yesconfirm=true, plus confirm_foreign=true for cm_absence, because it belongs to cm-office rather than to you. That second gate exists precisely so nobody scribbles on a shared calendar by accident.

Every real write is appended to calmcp's audit log. Re-running is safe: event UIDs are derived from the trip-folder name, so a repeat updates the existing event instead of duplicating it — and a rebooking becomes a move_event on the same UID rather than a second entry.

Your password is in your OS keyring. It is never in the repo, never in identity.yaml, never in an environment variable, and never in the conversation.

Behaviour is specified in prompts/60_calendar.md.

Fallback: no MCP server

scripts/add_to_calendar.py still works for sessions without the MCP server — another vendor's agent, a bare chat, GWDG Chat AI. It covers the absence block only (CalDAV, all-day or timed, confirm-first, same UID scheme):

pip install caldav icalendar --break-system-packages

python scripts/add_to_calendar.py <trip-folder>             # preview, no password
python scripts/add_to_calendar.py <trip-folder> --confirm   # push
python scripts/add_to_calendar.py --list-calendars          # find names / URLs
python scripts/add_to_calendar.py <trip-folder> --delete --confirm

On this path the connection details come from config/<institute>.yaml (kalender: fallback_caldav:) and your login from identity.yaml (kalender:). Leave app_password blank: the agent writes a push_calendar.command into the trip folder, you double-click it, and a native OS dialog collects the password locally — it never reaches the LLM.

Kerio Connect + Active Directory: Kerio app passwords do not work for AD-imported accounts, due to a known bug in the Active Directory Extension for Kerio Connect (KADE); use your regular password when prompted. This was the original reason for the runtime prompt — with the MCP server the password sits in the keyring and the problem doesn't arise.

Per-trip folder convention (the agent maintains this for you):

yyyymm_LOCATION_EVENT/
├── trip.md
├── 1_Invitation/
├── 2_Application/
├── 3_Booking/
├── receipts/
├── 5_Expense_Report/
└── 6_Followup/

Backlog import (old trips)

To bring trips that already happened into the convention (so they show up in records and, later, the dashboard), point the importer at a single-trip folder:

python scripts/backlog_trip.py <trip-folder>             # PREVIEW: facts + gaps, change nothing
python scripts/backlog_trip.py <trip-folder> --confirm   # apply: write trip.md + sort files

Preview is the default. It prints the gleaned facts (status, trip number, event, destination, dates, cost center, purpose — each tagged with where it came from: [application] / [folder name] / [default]), an explicit Missing / needs checking list of whatever it couldn't determine, and the exact changes it would make. Nothing is written until you pass --confirm.

It recovers what it can without asking — the trip number from the Reisestelle settlement-PDF filename, and the destination / purpose / dates from the signed application's front page (via pdftotext) — sets a headline status (closed only with settlement proof, otherwise open-unsure for an old import) and a milestones: block (antrag, genehmigt, booked, event taken place, abrechnung, erstattet) inferred conservatively from the files, writes a trip.md marked backlog_imported: true, and moves confidently-classified files into the subfolders. It's lenient and non-destructive: nothing is overwritten, ambiguous files are left at the top level and reported. Scope is single-trip folders; year-aggregator folders (many trips in one folder) and loose top-level settlement PDFs are not handled yet. Behaviour is in prompts/40_backlog.md.

Dashboard (overview across trips)

A portable, read-only overview built from the trip.md headers:

python scripts/dashboard.py <trips-root>             # writes <trips-root>/dashboard.html
python scripts/dashboard.py <trips-root> --text      # also print a text table
python scripts/dashboard.py <trips-root> --open      # open the HTML in the browser

It scans <trips-root> recursively for trip.md, then writes one self-contained HTML file (inline CSS/JS — no internet, no CDN) that opens in any browser on macOS, Linux, or Windows; the table filters and sorts. A --text mode prints the same as a terminal table. Pure Python stdlib + PyYAML, no LLM involved in viewing it.

It's action-first: trips needing attention float to the top, with chips for the alerts that matter — Abrechnung deadlines (3-month rule), missing/ unapproved applications as a trip nears, filed-but-not-reimbursed, abstract-submission deadlines, and registration / early-bird deadlines (from the anmeldung: block in trip.md). The type of contribution (invited / plenary / keynote / talk / poster, from the beitrag: block) shows as a badge next to each event. The dashboard is read-only: spot something, edit that trip's trip.md, and re-run. Only trips that have a trip.md appear, so old folders show up once imported with backlog_trip.py. Behaviour is in prompts/50_dashboard.md.

Using with Claude

How Claude finds the skill — two mechanisms

There are two ways Claude picks up this pilot, and which one applies depends on where the session runs. Getting this wrong is the single most common reason a session goes badly: Claude starts blind, ignores the scripts, and tries to hand-edit the DOCX.

Mechanism What it is Works in
The installed skill (skill/SKILL.md) Fires on "Dienstreiseantrag", "expense report", a yyyymm_LOCATION_EVENT/ folder, and so on. Locates the repo, then reads the full spec from it. everywhere — Cowork in the cloud, Cowork on your computer, Claude Code
CLAUDE.md at the workspace root Read automatically at session start; chains STATUS.mdSKILL.md00_pilot.mdlearnings.mdidentity.yaml. Claude Code (CLI), and Cowork tasks running on your computer

The important caveat. A Cowork task running in the cloud reaches your disk over the device bridge, and does not auto-load a CLAUDE.md sitting in a connected folder. If that's how you work — the default when you start a task from a phone or browser — installing the skill is what makes the pilot work at all. Verified July 2026.

Do both. The skill is the mechanism that always works; CLAUDE.md costs nothing and is what makes claude in that folder behave correctly.

Claude Cowork

  1. Connect the folder that contains both travel-forms-pilot/ and your trip folders — e.g. ~/Desktop/MPIE/TRAVEL-FORMS. Connect the parent, not the repo: Claude needs to see the trip folders too.
  2. Install the skilltravel-forms-pilot/skill/SKILL.md, packaged as a .skill file and saved to your account (installation step 4 above). This is what makes cloud sessions find the pilot.
  3. Copy CLAUDE.md.example to the connected folder root as CLAUDE.md and adjust the paths. Belt and braces — see the caveat above.
  4. Create personal/identity.yaml from identity.example.yaml and fill it in.
  5. Install the calendar MCP server if you want calendar entries (installation step 3 above).
  6. Pick a model. The work is mostly orchestration; a small fast model is plenty for routine trips. Reach for a larger one for novel reimbursement questions, complex multi-stop trips, or backlog cleanup.

Then just say what you need:

"New trip: 202609_ISAM5_Tokyo/ — I dropped the invitation in there."

No preamble. The skill answers in English, accepts German or English input, and asks once per session whether to fill the official forms in German (default — your Reisestelle prefers it) or English.

Cloud or on your computer? Both work. Running on your computer means Claude touches your files directly and CLAUDE.md is picked up automatically; running in the cloud means the skill is doing the work of finding the pilot. If a cloud session seems not to know about the pilot, that's the thing to check first.

Claude Code (CLI / IDE integration)

cd ~/Desktop/MPIE/TRAVEL-FORMS
claude     # auto-reads CLAUDE.md in the current folder

CLAUDE.md is picked up on startup, so no "read SKILL.md" instruction is needed. The AskUserQuestion UI becomes plain numbered questions; bootstrap, file sorting, form generation and the calendar flow are identical. Add the calendar server with:

claude mcp add calmcp \
  --env CALMCP_REGISTRY=/path/to/calendar-mcp-server/calendars.yaml \
  -- /path/to/calendar-mcp-server/.venv/bin/calmcp-mcp

claude.ai (web, no file access)

Without local file access the LLM can still help you compose the YAML config:

  1. Paste the contents of SKILL.md, prompts/00_pilot.md, and your personal/identity.yaml (with the personnel number masked if you like) into the system prompt or the first turn.
  2. Describe the trip.
  3. Claude asks the missing questions and produces a YAML block you can copy.
  4. Save the YAML locally and run python scripts/fill_application.py --config your.yaml --output-dir ./out yourself.

Slower than the agent setups, but works in any chat-only environment.

Using with GWDG Chat AI

GWDG (https://chat-ai.academiccloud.de) is a great fit for academic users with data-protection concerns, particularly Max-Planck-Society members for whom ChatGPT-class models are free. Three modes:

Mode A — Chat AI web UI (read-only mode, works for everyone)

The web UI lets you upload files as context but can't write to your disk or run shell commands. So you run the deterministic scripts locally and use the LLM only for the parts that need judgement:

  1. In a local terminal, scaffold the trip folder yourself:
    mkdir ~/Desktop/Trips/202609_Cargese_MecaNano-school
    # drop your invitation / programme / booking PDFs into that folder
    python travel-forms-pilot/scripts/bootstrap_trip.py ~/Desktop/Trips/202609_Cargese_MecaNano-school
  2. In Chat AI: paste SKILL.md and prompts/00_pilot.md into the System prompt.
  3. Attach the freshly-generated trip.md, your identity.yaml, and any invitation/programme PDFs.
  4. Pick a model — Llama 3.x 70B, Qwen 2.5 72B, or GPT-class (if you qualify) all work.
  5. The model proposes file-sort moves and a YAML config in the chat.
  6. Run the proposed mv commands and python scripts/fill_application.py --config that.yaml --output-dir ./out in your terminal.

This is the universally available mode. No local agent, no MCP, just two windows: a browser and a terminal.

Mode B — GWDG SAIA API + a local agent

GWDG's SAIA API is OpenAI-compatible. Point any local agent that supports custom OpenAI endpoints at it (Continue.dev, Cursor with custom OpenAI base URL, a thin home-grown wrapper). The agent then has full file + shell access locally while inference runs on GWDG infrastructure. This is the closest you get to the Claude Cowork experience with GWDG models.

OPENAI_API_BASE = https://chat-ai.academiccloud.de/v1
OPENAI_API_KEY  = <your AcademicCloud API token>

(Exact endpoint format and token issuance per the SAIA docs at https://docs.hpc.gwdg.de/services/ai-services/saia/.)

Mode C — CoCo AI in VSCode

If you already use VSCode, the CoCo AI extension (https://docs.hpc.gwdg.de/services/ai-services/coco/) gives you GWDG models inside the editor. Combined with VSCode's integrated terminal, this is essentially Claude Code with GWDG models. Open the repo as a workspace, point the chat at SKILL.md, and proceed.

Limitations to know about

  • The Chat AI web UI does not support localhost MCP servers, so you can't connect a local-filesystem MCP server to it. Public HTTPS MCP servers are supported but irrelevant here.
  • Tools (web search, image gen) in the web UI work only on GWDG-hosted models, not on the OpenAI passthrough.

Using with other LLMs

The skill is plain Markdown; the scripts are plain Python. Any LLM that can read files and run a shell command can drive the pilot.

Required capabilities

Capability Why Substitute if missing
Read / write / move files in the trip folder Read trip.md, identity.yaml, the invitation/programme; enrich trip.md; move loose files into subfolders None — must have this
Run a subprocess (python … / mv …) Call bootstrap_trip.py, fill_application.py, fill_expense.py; move sorted files User runs the scripts and mv commands; LLM only proposes them
Fetch a URL as text Pull conference programme snapshots when the invitation links to a page rather than attaches it User pastes the relevant text into the chat
Structured multi-choice UI (AskUserQuestion) Fast batched user input Plain numbered list — slightly slower but works
MCP client (for calmcp) Calendar entries — absence block and itinerary scripts/add_to_calendar.py for the absence block; no itinerary events

Known-good environments

Environment File access Shell Calendar Notes
Claude Cowork (on your computer) yes yes MCP CLAUDE.md auto-loaded. AskUserQuestion native.
Claude Cowork (in the cloud) via the device bridge yes MCP CLAUDE.md is not auto-loaded — install SKILL.md as a skill.
Claude Code (CLI) yes yes MCP CLAUDE.md auto-loaded. Plain Q&A instead of AskUserQuestion.
ChatGPT desktop / Custom GPTs optional optional MCP or script Connect a folder; Python tool runs scripts.
Cursor / Continue.dev / Aider yes yes script Treat the trip folder as the workspace.
Gemini Code Assist yes (Workspace) yes script Flash-class model is enough.
GWDG Chat AI web UI upload only no manual Read-only mode; user runs scripts locally. No localhost MCP.
GWDG SAIA API + local agent depends on agent depends on agent depends on agent OpenAI-compatible endpoint.
Bare chat (any LLM, no tools) no no manual LLM produces YAML; user saves and runs.

Model size guidance

Most of the work is mechanical: run bootstrap_trip.py, sort a handful of files into subfolders, read a few text files, ask four questions, write small YAML, call the fill script. That's well within the smallest tool-using models:

  • Routine new trip / new expense report → small/fast model (Claude Haiku 4.5, GPT-5-mini, Gemini Flash, Llama 3.x 70B, Qwen 2.5 72B).
  • First-time adaptation for a new institute → medium model (Claude Sonnet, GPT-5, Gemini Pro).
  • Backlog cleanup with messy mixed receipts and multiple trips at once → medium model.

Adoption for other institutes

This repo is opinionated toward MPI-SusMat. To adapt:

  1. Fork the repo.
  2. Replace templates/Dienstreiseantrag_*.docx and templates/Reiseabrechnung_*.docx with your institute's forms.
  3. Re-extract the field index tables for the new templates (recipe in docs/formular_mechanik.md).
  4. Edit config/<your-institute>.yaml: mail addresses, invoice address, EU list (if your country uses a different definition), house rules (per-diem rules, hotel-cap thresholds, kilometre rates).
  5. Update the template: and formulare: entries in SKILL.md if the filenames differ.
  6. Point config/<your-institute>.yaml kalender: at your own calendar ids (absenz_calendar_id / reise_calendar_id) as defined in your calendars.yaml.

The trip-folder convention, the per-trip trip.md template, the prompt scaffolding, and the form-fill engine (_docx_form.py) all stay as-is.

Status

  • Minimal-work folder onboarding: bootstrap_trip.py scaffolds subfolders, copies trip.md, pre-fills date/location/event from the folder name; agent does the file sorting and trip.md enrichment.
  • Scripted single-shot form fill: tested end-to-end against the MPIE templates (May 2026). Cargèse (EU, A1 mode) → 2-page PDF. FAU Erlangen (domestic) → 1-page PDF.
  • Reiseabrechnung header-field mapping is documented; the 130+ per-day repeating fields use raw 0-based indices, to be aliased as needed.
  • Calendar via the calmcp MCP server (July 2026): absence block on the shared calendar (double-confirmed), itinerary events on your own, both dry-run first and idempotent by trip-folder UID. add_to_calendar.py retained as the CalDAV fallback for sessions without an MCP server.
  • Personal data separated from the repo: identity.yaml / bonus_points.md resolved through a documented search path, with *.example.* counterparts tracked in git.
  • Backlog import (backlog_trip.py): lenient importer for old single-trip folders — infers status, recovers trip number / destination / dates / purpose, fills trip.md, sorts files. Non-destructive; previews by default, writes only with --confirm.
  • Dashboard (dashboard.py): portable, self-contained HTML overview of all trips with action-first alerts (deadlines, application gaps, registrations, pending reimbursements) + a --text mode. Stdlib + PyYAML, cross-OS.
  • Not yet built: backlog for year-aggregator folders + loose settlement PDFs, quickie-receipt-photo workflow.

Contributing

Pull requests welcome — especially:

  • Field-mapping refinements for the Reiseabrechnung's per-day blocks (add to NAMED_FIELDS in scripts/fill_expense.py).
  • New institute config files (config/<institute>.yaml).
  • Trip-specific learnings worth generalising (add to learnings.md with a short PR description).

License

MIT. No warranty.

Acknowledgements

Built on top of the German Dienstreise paperwork conventions used at MPI-SusMat / MPI Eisenforschung Düsseldorf. The MPIE form templates remain the property of MPI and are included here only as the working substrate the skill drives — they may not be redistributed under the MIT licence; replace them with your own institute's forms when forking outside MPI-SusMat.

About

Skill & helper functions to fill out travel and settlement applications, designed for MPI-SusMat but generalisable

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages