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.
For each business trip, the pilot manages the whole lifecycle:
- A per-trip
trip.md(YAML header + free-text sections) that travels with the trip from invitation to reimbursement. - 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).
- The official Reiseabrechnung (expense report) DOCX.
- A running ledger of business-earned bonus points (BahnBonus, Miles & More) that have to be reported to the institute.
- 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
calmcpMCP 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.
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.mdchanges, the assistant re-runsdashboard.pysodashboard.htmlalways reflects the latest state — and you can regenerate it yourself at any time.
- Create a folder named
yyyymm_LOCATION_EVENT/and drop in whatever you have (invitation, programme, links). - Open the assistant in your
TRAVEL-FORMSdirectory and point it at the folder. It scaffolds the folder, sorts your files, fills intrip.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. - 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).
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.
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.
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.
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.
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.
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-pilotpyyaml 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.
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-packagesDrop --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 libreofficeIf 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.appCheck 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.
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.yamlidentity.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.
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 reachableThe 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).
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.
"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.
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.
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 ./outOutput:
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.
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)
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.mdWhere 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.
The user does the absolute minimum per trip:
- Create a folder named
yyyymm_LOCATION_EVENT/somewhere on disk (e.g.~/Desktop/Trips/202609_Cargese_MecaNano-school/). - Drop stuff into it — invitations, programme PDFs, train/flight bookings, receipt photos — at the top level. No manual sorting.
- Tell the LLM: "New trip, here's the folder."
From there the agent takes over:
- Runs
scripts/bootstrap_trip.pywhich creates the canonical subfolders (1_Invitation/,2_Application/,3_Booking/,receipts/,5_Expense_Report/,6_Followup/), copiestrip.md.tmplinto the folder astrip.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.pyand 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.
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.
Both go through the calmcp calendar MCP server
(installation in step 3 above). The assistant:
- Dry-runs the write.
calmcpreturns a before/after contract and changes nothing. - Shows you the proposed event, with weekdays on the dates.
- Writes only after your yes —
confirm=true, plusconfirm_foreign=trueforcm_absence, because it belongs tocm-officerather 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.
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 --confirmOn 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/
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 filesPreview 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.
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 browserIt 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.
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.md → SKILL.md → 00_pilot.md → learnings.md → identity.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.
- 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. - Install the skill —
travel-forms-pilot/skill/SKILL.md, packaged as a.skillfile and saved to your account (installation step 4 above). This is what makes cloud sessions find the pilot. - Copy
CLAUDE.md.exampleto the connected folder root asCLAUDE.mdand adjust the paths. Belt and braces — see the caveat above. - Create
personal/identity.yamlfromidentity.example.yamland fill it in. - Install the calendar MCP server if you want calendar entries (installation step 3 above).
- 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.
cd ~/Desktop/MPIE/TRAVEL-FORMS
claude # auto-reads CLAUDE.md in the current folderCLAUDE.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-mcpWithout local file access the LLM can still help you compose the YAML config:
- Paste the contents of
SKILL.md,prompts/00_pilot.md, and yourpersonal/identity.yaml(with the personnel number masked if you like) into the system prompt or the first turn. - Describe the trip.
- Claude asks the missing questions and produces a YAML block you can copy.
- Save the YAML locally and run
python scripts/fill_application.py --config your.yaml --output-dir ./outyourself.
Slower than the agent setups, but works in any chat-only environment.
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:
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:
- 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
- In Chat AI: paste
SKILL.mdandprompts/00_pilot.mdinto the System prompt. - Attach the freshly-generated
trip.md, youridentity.yaml, and any invitation/programme PDFs. - Pick a model — Llama 3.x 70B, Qwen 2.5 72B, or GPT-class (if you qualify) all work.
- The model proposes file-sort moves and a YAML config in the chat.
- Run the proposed
mvcommands andpython scripts/fill_application.py --config that.yaml --output-dir ./outin your terminal.
This is the universally available mode. No local agent, no MCP, just two windows: a browser and a terminal.
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/.)
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.
- 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.
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.
| 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 |
| 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. |
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.
This repo is opinionated toward MPI-SusMat. To adapt:
- Fork the repo.
- Replace
templates/Dienstreiseantrag_*.docxandtemplates/Reiseabrechnung_*.docxwith your institute's forms. - Re-extract the field index tables for the new templates (recipe in
docs/formular_mechanik.md). - 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). - Update the
template:andformulare:entries inSKILL.mdif the filenames differ. - Point
config/<your-institute>.yamlkalender:at your own calendar ids (absenz_calendar_id/reise_calendar_id) as defined in yourcalendars.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.
- Minimal-work folder onboarding:
bootstrap_trip.pyscaffolds subfolders, copiestrip.md, pre-fills date/location/event from the folder name; agent does the file sorting andtrip.mdenrichment. - 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
calmcpMCP 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.pyretained as the CalDAV fallback for sessions without an MCP server. - Personal data separated from the repo:
identity.yaml/bonus_points.mdresolved 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, fillstrip.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--textmode. Stdlib + PyYAML, cross-OS. - Not yet built: backlog for year-aggregator folders + loose settlement PDFs, quickie-receipt-photo workflow.
Pull requests welcome — especially:
- Field-mapping refinements for the Reiseabrechnung's per-day blocks (add to
NAMED_FIELDSinscripts/fill_expense.py). - New institute config files (
config/<institute>.yaml). - Trip-specific learnings worth generalising (add to
learnings.mdwith a short PR description).
MIT. No warranty.
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.