Vaudtax

Other

Working with .vaudtax files (Swiss canton Vaud tax declarations). Use when the user mentions a .vaudtax file, wants to read/summarize/convert a VD tax declaration, or inspect tax data (income, deductions, assets, attached documents).

Install

openclaw skills install vaudtax

VaudTax Skill

File format

A .vaudtax file is a ZIP archive containing:

  • One XML file (named <filename>.xml) — the entire tax declaration as structured XML under the namespace http://www.vd.ch/fiscalite/vaudtax, root element <vaudTaxData>.
    • Namespace: http://www.vd.ch/fiscalite/vaudtax (proprietary format, no public XSD available)
    • Structure: 32+ main sections organizing all tax declaration data
  • Zero or more doc* files — attached supporting documents: PDF, JPEG, or PNG. The format is declared in the XML's <mimeType> field.

Bundled scripts

All scripts live in the scripts/ subdirectory of this skill and use only Python standard library — no installs needed.

ScriptPurpose
parse_vaudtax.py <file.vaudtax>Parse and print a human-readable summary to stdout
export_json.py <file.vaudtax> [out.json]Export clean JSON (omits UI/navigation state)
compute_code800.py <file.vaudtax>Estimate revenu imposable ICC (code 800), IFD, and fortune — outputs values ready to pass to calculate_taxes.py
calculate_taxes.py --periode YEAR --commune NAME --revenu-icc N --fortune-icc N --revenu-ifd NQuery the official Canton Vaud tax calculator via HTTP POST and return authoritative results

The JSON output conforms to vaudtax-export.schema.json (JSON Schema 2020-12; file is in the skill root).

SCRIPTS=$(find ~ -name parse_vaudtax.py -path '*/vaudtax/*' 2>/dev/null | head -1 | xargs dirname)
python "$SCRIPTS/parse_vaudtax.py" /path/to/file.vaudtax
python "$SCRIPTS/export_json.py"   /path/to/file.vaudtax

Key XML sections

Metadata & Taxpayer Info

SectionDescription
fiscalPeriod✅ Tax year (e.g. 2025)
lastGesdemReference✅ Gesdem submission reference
identification✅ Address, municipality, phone, email, IBAN
taxpayerPersonalData1✅ Name, birthdate, NAVS13, profession, marital status
taxpayerPersonalData2✅ Second taxpayer (joint filers only)
representativeTax representative details (not yet parsed)

Income

SectionDescription
activiteSalarieeRevenus✅ Employed income: employer, net salary, pension contributions, dates, activity rate
complementRentePension✅ Pension/rente income: type, annual amount
activitesIndependantes✅ Self-employment income: activity name, net revenue
autresRevenusExoneresImposesSourceOther income (not yet parsed)
revenuImposeAutreEtatIncome taxed in other states (not yet parsed)

Deductions & Expenses

SectionDescription
autresFraisEtFraisActiviteSalarialeAccessoire✅ Professional expense deduction method (flat-rate or actual)
fraisTransport✅ Transport costs: type, km, number of days, route
fraisRepas✅ Meal costs: type, number of days
primesEtCotisationsAssurance✅ Insurance premiums, subsidies, 3rd pillar (3a) contributions
deductionSocialeLogement✅ Rent/housing deduction
fraisMedicauxDentaires✅ Medical and dental expenses
interetsDettes✅ Debt interest deductions (code 520)
fraisFormation✅ Training/education costs
donationsAvancesHoiries✅ Donations and inheritance advances
successionHoirieDonationInheritances flag (skipped when isInitialized=false)

Assets & Securities

SectionDescription
etatTitres✅ Bank accounts: IBAN, balance, yield
relevesFiscauxBancaires✅ Investment portfolios: fiscal value, gross income, IES
numerairesList✅ Cash and liquid assets
objetsMobiliers✅ Movable property / crypto
biensImmobiliers (2025) / immeubles (older)✅ Real estate: commune, parcelle, fiscal value, rental income
autoMoto✅ Vehicles
fraisAdministrationTitres✅ Management fees for securities (code 490)

Supporting Documents & Navigation

SectionDescription
piecesJustificativesObligatoiresMandatory supporting document metadata
piecesJustificativesFacultativesOptional supporting document metadata
infosComplementairesIesAdditional investment income (IES) information
prestationsEnCapitalCapital benefit payments (not yet parsed)
guidedNav / userProfil / piecesJustificativesSubFormInitializedUI/navigation state — intentionally skipped

Summarizing a file

Run parse_vaudtax.py and present the output using this structure:

  1. Filing info — fiscal year, reference, municipality
  2. Taxpayer(s) — name, birthdate, civil status, profession; CTB2 if joint filer
  3. Income — employed activity, pension/rentes, self-employment
  4. Deductions — transport, meals, professional expenses, insurance/3rd pillar, rent, medical, debt interest, education
  5. Assets — bank accounts, investment portfolios, cash, real estate, vehicles, movable objects
  6. Attached documents — filename and size for each
  7. Cross-check results — outcome of proactive PDF verification (see below)

Amounts are in CHF unless a different devise is specified.

Only report what exists. Do not mention sections that are empty, not initialized, or not applicable to this taxpayer. If parse_vaudtax.py reports no unknown sections, say nothing about sections at all.

A summary is just a summary. Do not run compute_code800.py or calculate_taxes.py unless the user explicitly asks for a tax estimate.

Proactive cross-checks

Even for a basic summary, always cross-check attached documents against their XML values — discrepancies are high-value findings the user needs before filing. Run all PDF reads in parallel (spawn one sub-agent per document or issue all open_attachment + read_pdf calls in a single parallel batch).

Pillar 3a attestations (label contains "21 EDP", "cotisations", or "pilier 3a"):

  1. Read each PDF with read_pdf() + extract_form21_totals() (or extract_postfinance_3a() for PostFinance) — see references/pillar-attestation.md
  2. Sum per taxpayer; compare against formesReconnuesPrevoyanceIndividuelleContribuable1 / ...Contribuable2
  3. Flag any discrepancy: "⚠ Écart pilier 3a : attestations CHF X, déclaration CHF Y — vérifier avant envoi"

Salary certificates (label contains "Certificat de salaire"):

  1. Read the PDF and extract line 11 (salaire net) and line 10.1 (LPP) — see references/salary-certificate.md
  2. Compare against salaireNet and cotisationOrdinaire in the XML
  3. Flag any mismatch beyond ±1 CHF rounding

Skip the cross-checks only if the user explicitly asks for a quick overview.

When verifying deductions, see references/deductions.md for official rules and caps.

Full analysis — always include taux marginal

For any full analysis (running both compute_code800.py and calculate_taxes.py), always also compute the taux marginal with --marginal-rate:

python calculate_taxes.py \
  --periode YEAR --commune "Commune" \
  --revenu-icc N --fortune-icc N --revenu-ifd N \
  --marginal-rate

This makes two HTTP calls and returns the marginal rate for ICC, IFD, and the combined total. Include it in the output:

Taux marginal
ICC (cantonal + communal)X.XX %
IFD (fédéral direct)X.XX %
TotalX.XX %

See references/tax-computation.md for ICC and IFD formulas.

Reading attached PDFs

Use the bundled pdf_utils.py (in the skill's scripts/ directory):

from pdf_utils import read_pdf, extract_form21_totals, identify_taxpayer

text = read_pdf("/tmp/doc.pdf")               # text PDF or scanned — handled automatically
text = read_pdf("/tmp/doc.pdf", lang="deu")   # switch to German if needed

read_pdf() tries pdfplumber first; falls back to pytesseract OCR at 200 dpi. Use lang="fra" (default) for French, "deu" for German.

Read multiple attachments in parallel — spawn one sub-agent per document or issue all open_attachment + read_pdf calls concurrently. Wall-clock time scales with the slowest single document, not the total count.

For salary certificate field layout → references/salary-certificate.md For Form 21 EDP pillar attestation structure → references/pillar-attestation.md

Extracting attached files

Always use the open_attachment() context manager from parse_vaudtax.py — it extracts to a temp file and deletes it on exit:

from parse_vaudtax import open_attachment
from pdf_utils import read_pdf

with open_attachment("file.vaudtax", "doc17700000000000", suffix=".pdf") as path:
    text = read_pdf(path)

Use the <key> value (not the <reference> UUID) to match XML metadata to ZIP entries. Each <documents> element has: <key>, <filename>, <mimeType>, <label>, <fileSize>.

VaudTax XML Schema

Proprietary format maintained by the Canton Vaud tax authority — no public XSD. Element names follow French naming conventions and may change across fiscal years.

Known differences between fiscal years

Section2023–20242025
Medical expensesfraisMedicauxfraisMedicauxDentaires
Medical net amountmontantAChargemontantFrais
Real estate sectionimmeublesbiensImmobiliers
Real estate fiscal valuevaleurFiscaleestimationFiscale

parse_vaudtax.py handles both variants transparently.

If a parsed field returns None unexpectedly, inspect actual child element names:

import zipfile, xml.etree.ElementTree as ET
NS = "http://www.vd.ch/fiscalite/vaudtax"
with zipfile.ZipFile("file.vaudtax") as z:
    xml_name = next(n for n in z.namelist() if n.endswith(".xml"))
    root = ET.parse(z.open(xml_name)).getroot()
for el in root.findall(f"{{{NS}}}sectionName"):
    print({c.tag.split("}")[1]: c.text for c in el})

Official references

ResourceURLNotes
Instructions générales 202521001_2025.pdfMain guide; URL is year-specific
ICC barème revenu 2025barème_revenu_2025.pdfIncome tax table at CHF 100 intervals
ICC barème fortune 2025barème_fortune_2025.pdfWealth tax table at CHF 1'000 intervals
IFD barème 2025 (form 58c)Bareme_IFD_58c-2025.pdfSingle + married tables
Communal coefficientsArrêtés d'impositionCurrent year XLS for all communes

Update PDF URLs by replacing the year suffix (e.g. 21001_2024.pdf for fiscal year 2024).

Reporting unhandled content

When the file contains sections or fields the skill can't handle, tell the user and suggest opening a GitHub issue.

Report when:

  • parse_vaudtax.py outputs a "Sections non reconnues" block
  • A section marked "not yet parsed" above contains data (isInitialized is not false and child elements are non-empty)
  • A script raises an exception or produces clearly wrong output

Say:

This declaration contains content the vaudtax skill doesn't handle yet: [describe what's missing]. The analysis above may be incomplete.

Please open an issue at https://github.com/fredj/ai-stuff/issues with: the fiscal year, the section name(s), and a brief description (no personal data needed).

Do not silently skip unhandled content.

Guardrails

Tax data is sensitive — being wrong is worse than being incomplete.

Sources — Every number must trace back to script output or a direct XML field read. Never supply a value from general knowledge. If a field is absent, say so — absent ≠ zero ≠ "not applicable".

No mental arithmetic — Never compute ICC/IFD/fortune tax without running compute_code800.py + calculate_taxes.py. The only exception is explaining the method conceptually.

Year-specific rules — Always read fiscalPeriod before applying any deduction cap, barème table, or form field name. The limits in references/deductions.md are 2025 values and differ for other years.

No guessing — Never infer what an unhandled section contains, never fabricate totals from partial data, never guess XML field names (use the discovery snippet above).

Estimates vs official figurescompute_code800.py output is an estimate. When a lastGesdemReference or official bordereau is available, those figures take precedence.

Commune rates — Tax coefficients are commune-specific. Flag any mismatch between the commune in identification and the one passed to calculate_taxes.py.

Réforme valeur locative (from 2029) — Valeur locative suppressed, mortgage interest non-deductible (except primo-acquéreurs Art. 33a LIFD), code 660 disappears for IFD. Do not apply pre-2029 rules to post-2028 projections.

Network — The only network call is calculate_taxes.pyhttps://www.vd.ch/.... It sends three integers, the commune name, and marital status — no personal identifiers.

When in doubt: say what you found, say what you couldn't find, and let the user decide.