Install
openclaw skills install vaudtaxWorking 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).
openclaw skills install vaudtaxA .vaudtax file is a ZIP archive containing:
<filename>.xml) — the entire tax declaration as structured XML under the namespace http://www.vd.ch/fiscalite/vaudtax, root element <vaudTaxData>.
http://www.vd.ch/fiscalite/vaudtax (proprietary format, no public XSD available)doc* files — attached supporting documents: PDF, JPEG, or PNG. The format is declared in the XML's <mimeType> field.All scripts live in the scripts/ subdirectory of this skill and use only Python standard library — no installs needed.
| Script | Purpose |
|---|---|
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 N | Query 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
| Section | Description |
|---|---|
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) |
representative | Tax representative details (not yet parsed) |
| Section | Description |
|---|---|
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 |
autresRevenusExoneresImposesSource | Other income (not yet parsed) |
revenuImposeAutreEtat | Income taxed in other states (not yet parsed) |
| Section | Description |
|---|---|
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 |
successionHoirieDonation | Inheritances flag (skipped when isInitialized=false) |
| Section | Description |
|---|---|
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) |
| Section | Description |
|---|---|
piecesJustificativesObligatoires | Mandatory supporting document metadata |
piecesJustificativesFacultatives | Optional supporting document metadata |
infosComplementairesIes | Additional investment income (IES) information |
prestationsEnCapital | Capital benefit payments (not yet parsed) |
guidedNav / userProfil / piecesJustificativesSubFormInitialized | UI/navigation state — intentionally skipped |
Run parse_vaudtax.py and present the output using this structure:
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.
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"):
read_pdf() + extract_form21_totals() (or extract_postfinance_3a() for PostFinance) — see references/pillar-attestation.mdformesReconnuesPrevoyanceIndividuelleContribuable1 / ...Contribuable2Salary certificates (label contains "Certificat de salaire"):
references/salary-certificate.mdsalaireNet and cotisationOrdinaire in the XMLSkip 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.
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 % |
| Total | X.XX % |
See references/tax-computation.md for ICC and IFD formulas.
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
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>.
Proprietary format maintained by the Canton Vaud tax authority — no public XSD. Element names follow French naming conventions and may change across fiscal years.
| Section | 2023–2024 | 2025 |
|---|---|---|
| Medical expenses | fraisMedicaux | fraisMedicauxDentaires |
| Medical net amount | montantACharge | montantFrais |
| Real estate section | immeubles | biensImmobiliers |
| Real estate fiscal value | valeurFiscale | estimationFiscale |
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})
| Resource | URL | Notes |
|---|---|---|
| Instructions générales 2025 | 21001_2025.pdf | Main guide; URL is year-specific |
| ICC barème revenu 2025 | barème_revenu_2025.pdf | Income tax table at CHF 100 intervals |
| ICC barème fortune 2025 | barème_fortune_2025.pdf | Wealth tax table at CHF 1'000 intervals |
| IFD barème 2025 (form 58c) | Bareme_IFD_58c-2025.pdf | Single + married tables |
| Communal coefficients | Arrêtés d'imposition | Current year XLS for all communes |
Update PDF URLs by replacing the year suffix (e.g. 21001_2024.pdf for fiscal year 2024).
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" blockisInitialized is not false and child elements are non-empty)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.
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 figures — compute_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.py → https://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.