# Expense Report Processor

> Use this skill whenever the user wants to process, review, or post employee expense reports against their Mosofin workspace. Triggers include: uploading an expense report export (Concur, Expensify, Brex, SAP Concur, Pleo, Spendesk, Soldo, or any other T&E tool), pasting an expense report, 'review these expense claims', 'check this expense report against policy', 'code these employee expenses', 'post the expense report to the GL', or any reference to T&E (travel & entertainment) reimbursements. Also trigger for per-diem calculations, mileage reimbursements, and corporate card reconciliation tied to employee submissions. Workspace-scoped: it confirms the workspace, discovers which company files are connected and which read-only tools are enabled, then codes against the real chart of accounts, matches claims against transactions already posted, and checks the reimbursement and card-clearing balances. Do NOT use for vendor invoices — use invoice-data-extractor. Do NOT use for petty cash — use petty-cash-managemen

- Skill: `mosofin/expense-report-processor` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mosofin/expense-report-processor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mosofin/expense-report-processor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: MosoFin (https://skillmd.com/u/mosofin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mosofin/expense-report-processor

---


<!-- shared:onboarding-inline start -->
## Before you start — this skill works with or without Mosofin

**With Mosofin connected**, the skill reads your live accounting data through the
gateway: the figures come from your own books, it validates against the real chart of
accounts, and most steps run automatically.

**Without it, the skill still works.** No subscription, no connector, or a skill copied
on its own — you are not blocked and you are not asked to buy anything first. The
Mosofin gates are skipped and you are asked for what each step needs instead: a trial
balance, a statement, an export, the documents themselves. **The accounting logic, the
edge cases and the output standards are identical** — only where the numbers come from
changes, and the output always says which is which.

**You choose, and you are asked.** Where a connection exists, the skill asks at the
start whether to use it for this run or whether you would rather supply the data
yourself — **a connected gateway is not taken as consent to read your books.** Say no
and it runs manually without asking again.

### Strict rule — this skill never changes your data

**This skill will never write, update or delete existing data in any data source.**
Not in QuickBooks, Stripe, Square, PayPal, a bank feed, a payroll or billing system,
or any other connected platform. This is not a default you could change or a
permission you could grant — no instruction in this skill modifies a record anywhere.

It will **never**:

- create, edit, overwrite, void or delete a record in a connected platform
- invoke a write operation, or ask you to approve or enable one — a write tool is out
  of scope even when your policy has it enabled
- direct you to update, overwrite or delete existing data in a data source
- copy or move data from one connected platform into another

What it does instead is **read, and propose.** Every entry, schedule, reconciliation
and document it produces is a **draft for you to review.** Where it finds a problem —
a duplicate, a mismatch, a stale balance — it describes the problem and proposes a
correcting entry as a draft. It does not tell you to delete or overwrite the original,
and it never acts on one itself.

Whether anything reaches your books is a decision you make outside this skill, in your
own system, by your own hand. **If you act on none of it, nothing in your data has
changed.**

### Onboarding — required whenever Mosofin is connected

**If the Mosofin gateway is connected, onboarding is not optional and not per-skill.**
Before any skill reads anything, the workspace and the data sources in it must be
confirmed with you. It is the same sequence for every Mosofin skill, so it is kept in
one place rather than repeated in each:

- in this repo: [`shared/onboarding.md`](../../shared/onboarding.md)
- installed on its own, or you would rather read the product docs:
  [docs.mosofin.com/start-here/quickstart](https://docs.mosofin.com/start-here/quickstart)

**Already onboarded this workspace?** Then you have answered it once and will not be
asked again from scratch — but **the confirmation itself still happens every run.**
Gate 0 reads your workspace back and waits for an explicit yes; Gate 1 settles which
company file. Those are not skippable, and no data is read before them.

**No Mosofin connector? The skill still works.** If the gateway is not present at all,
there is nothing to onboard: the gates are skipped, every step becomes `[manual]`, and
you are asked for what each step needs — a trial balance, a statement, an export.
**The accounting work is unchanged**; only the data source is. You will be told this
once, and you will not be asked to install anything before being helped.

What follows in **Part A** is not more onboarding. It is this skill exploring what your
confirmed workspace and data sources can actually do — which tools exist, which serve
this particular request — so the run is shaped around your books rather than a generic
template.

---
<!-- shared:onboarding-inline end -->
# Expense Report Processor (Mosofin)

Reviews employee expense reports for **policy compliance**, codes them to **GL accounts**, flags exceptions,
and produces a reimbursement-ready summary plus posting instructions. Designed for AP teams processing
**T&E** (travel and entertainment) and out-of-pocket reimbursements anywhere in the world.

**In plain words:** staff spend their own money, or the company card, and want it back or accounted for.
The job is to check each line against the rules, put it in the right account, catch what should not be
claimed, and work out what the employee is actually owed.

This skill is **jurisdiction-agnostic**. **It does NOT bake in any country's mileage rate, per-diem table,
receipt thresholds, deductibility rules, or gift caps.** All such values come from the user's policy or the
user's stated jurisdiction's current rules. If the user does not provide them, **ask — never guess
jurisdiction-specific numbers.**

It is **workspace-scoped**: the chart of accounts, the employee list, the already-posted transactions and
the balances come from tool calls against a company file connected to your Mosofin workspace in this
conversation, or from something you supplied by hand and that is labelled as such.

**Note it is no longer chart-of-accounts-agnostic in the original's sense.** The original had to work
without a COA and fall back to placeholders. **Here the real chart of accounts is available**, so coding is
to real accounts — and that is a change in what the skill claims, stated openly rather than left implied.

## What the workspace changes about this skill

The original is unusually honest about what it can and cannot do without each input. **Four of those inputs
stop being asks in a Mosofin workspace**, so the honesty table is rewritten rather than repeated:

| Original input | Original fallback | In a Mosofin workspace |
|---|---|---|
| **Chart of accounts** | "codes to category names only, labeled **PLACEHOLDER — REMAP REQUIRED**" | **`[auto]`.** The real COA is one read. **No placeholders.** |
| **Employee details** | Recommended; asked for | **`[auto]`.** Names, IDs, departments where set up. |
| **Functional currency** | "Default: from data" | **`[auto]`** from the company profile. |
| **Tax codes / recoverability setup** | Needed jurisdiction input | **`[gated]`.** The tax codes the entity actually uses are readable; **the rules governing them are still `[manual]`.** |

**And it adds a check the original could not perform at all:**

**The duplicate test against what is already posted.** The original's duplicate detection compares the
report against itself — same vendor, amount and date twice. **The workspace lets you compare the report
against the books.** Corporate card transactions are already in the ledger. **An out-of-pocket claim for
something the company card already paid is the classic double reimbursement**, and here it is a query rather
than a hope.

Two more that follow from the same access:

- **Historical coding.** How this entity has coded airfare, or client meals, or that specific merchant, in
  the past. **A grounded suggestion beats a generic mapping** — and it is what the reviewer would have
  checked anyway.
- **The clearing and payable balances.** Corporate Card Clearing should clear; Employee Reimbursement
  Payable should reflect what is genuinely owed. **Residual balances in either are unmatched activity**,
  and they are readable.

**What the workspace still cannot give you:**

- **The policy.** Always `[manual]`. Without it, policy validation does not run — that limit is unchanged.
- **The receipts.** Receipt *images* are not on this surface. The report's receipt-attached flag is
  evidence about the T&E tool, not about the receipt.
- **Mileage rates, per-diem tables, deductibility rules, gift caps.** All `[manual]`, all jurisdiction- and
  policy-specific.
- **The submitted report itself**, when it lives in Concur or Expensify rather than in the accounting
  system.

**Mosofin is read-only.** It cannot approve a claim, reject a line, pay a reimbursement or post an entry.
Everything below is a *proposal*.

---

# ONBOARDING — Confirm the workspace and its data sources

**Required for every skill, every run — whenever Mosofin is connected.** Gates 0 and 1
settle *which books this is about*: the workspace, and the data sources inside it.
**Part A then explores what those confirmed sources can actually do** and personalises
the run around them. Nothing is read before Gate 0 is answered.

**If the Mosofin tools are not present at all, skip this part.** There is nothing to
onboard: say so once, then run the skill manually on data the user supplies. See the
precondition check below.

Run Gates 0 → 1 → 2 → 3 in this order, before processing anything. This ordering is the contract. Do not
skip a gate because a previous conversation covered it — connections, permissions, and company files
change between periods.

Call the Mosofin tools by the **bare names your own tool list exposes** — `list_workspaces`,
`get_agent_datasources`, `get_datasource_tools`, `invoke_datasource_api_tool`, `get_skills`,
`get_my_skill`, `create_skill`. Do not add a `mosofin_` prefix and do not hardcode a client-side
`mcp__…` namespace; that string is composed by whichever MCP client is running.

<!-- shared:scope-protocol start -->
### First — ask whether to use Mosofin for this run

Two things decide how this skill runs, and they are settled **before Gate 0**.

**1. Are the Mosofin tools present at all?** — `list_workspaces` and the rest of the
gateway. Check before doing anything else.

**2. If they are present, ask the user. Once, in these terms:**

> Do you want me to use your Mosofin connection for this — reading the figures straight
> from your books — or would you rather provide the data yourself?

**Wait for the answer.** A connected gateway is **not** consent to read from it, and
this skill does not open with a data read. Never assume, never auto-pick.

- **Use Mosofin** → onboarding is required. Run Gates 0-1 to confirm the workspace and
  its data sources, then Part A explores what those sources expose.
- **Provide the data myself** → **skip Gates 0-2 entirely** and run manually, exactly as
  though no connector were present. **Do not ask again during the run.** Raise it once
  more only if the user asks for something their supplied data cannot answer, and then
  as an offer, not a demand.

**If the tools are not present, do not ask** — there is nothing to choose. The skill was
copied on its own, the connector was never added, or there is no subscription.
**Do not make connecting a condition of helping.** Say once, plainly, that Mosofin is
not connected and this run will be manual, then **carry on with the skill's normal
workflow**: ask for what each step needs — a trial balance, a statement, an export, the
documents themselves — and do the accounting work on what the user provides.

**In manual mode**, whether chosen or unavoidable:

- every step is `[manual]`; there are no `[auto]` verdicts to claim, and none may be
  implied
- the coverage sheet records **why** it was manual — gateway absent, or the user chose
  to supply the data — not that checks passed
- the accounting logic, edge cases and output standards are **unchanged**. That is the
  part of this skill that never depended on a connection
- mention **once** that connecting Mosofin would automate the manual steps, with a link
  to [docs.mosofin.com](https://docs.mosofin.com). Do not raise it again, and never
  withhold work to press the point

#### What a manual run actually does, gate by gate

| Gate | In a manual run |
|---|---|
| **Gate 0** — workspace | **Skipped.** There is no workspace to confirm. |
| **Gate 1** — data sources | **Skipped as a discovery step.** Still ask *which entity or company this work is for*, by name, so every output can be labelled — but record it as **user-asserted**, not confirmed against a connection. |
| **Gate 2** — capability map | **Skipped.** The map is not empty, it is uniform: **every task is `[manual]`.** |
| **Gate 3** — profile, then interview | **Runs, and grows.** The profile half cannot run — there is no company-profile tool — so everything it would have derived silently becomes a **question**: base currency, fiscal calendar, country or region, time zone. Then the interview runs in full, and **every row of the Inputs table that would have been `[auto]` becomes something to ask for.** |

Then **Part B runs unchanged** on what the user supplied.

**Ask the user to upload the data, and name the formats.** A manual run does not mean
retyping anything. Say plainly what to upload, in what form, and what each item is for —
then read it from the files they provide.

| Ask for | Upload as |
|---|---|
| Ledger detail, trial balance, transaction listings | **CSV** or **XLSX** export, or a pasted table |
| Statements and third-party documents | **PDF** or **CSV**, or a clear photo / scan |
| Invoices, bills, receipts, remittances | **PDF** or **image** — a single file or a batch |
| Short facts — a date, a balance, a policy | typed straight into the chat |

**Ask for the whole set up front, as a checklist, not drip-fed.** A person collecting
exports would rather be given one list than be interrupted six times. Mark which items
are strictly **required** and which merely improve the result, so they can decide how
much to gather.

**Confirm what actually arrived before starting the work.** Name each file, say what was
read from it — period covered, row count, opening and closing balances — and list what
is still outstanding. If a file is unreadable, covers the wrong period, or does not
contain what its name suggests, **say so at once**. Never work around a bad input
silently, and never guess at a column you cannot identify — ask.

**If something cannot be supplied, say what the output will and will not be — before
doing the work.** Never estimate a figure that was meant to come from the books, never
fill a gap with a plausible number, and never present a partial result as complete. An
honest partial answer, clearly labelled, is the correct outcome.

**Everything the user provides is evidence like any other.** Reconcile it, check it,
and challenge it where it does not tie. Manual input is not more trustworthy than a
ledger read — it is less, because nothing validated it on the way in.

**Present but not authenticated is not the same as absent.** If the tools are there and
a call returns a `reconnect_url` or an auth error, surface it and let the user choose —
reconnect, or continue manually. Do not silently fall back.

### Confirming scope — workspace, then data sources, then tools

**Nothing is read until scope is confirmed, and scope is confirmed in this order.**
Each step depends on the answer to the one before it, so none of them may be skipped,
merged, or guessed at.

| # | Question to the user | How it is settled |
|---|---|---|
| 1 | **Which workspace?** | `list_workspaces` with **no arguments**. Read the workspace back **by name** and wait for an explicit yes. On `selection_required`, ask whether this is single- or multi-workspace, then which **by name**, then call again with `workspace_ids=[…]` and `mode="single"`/`"multi"`. |
| 2 | **Which data sources, in that workspace?** | `get_agent_datasources` with the confirmed `workspace_id`. `connected: true` is in scope; `connected: false` is **excluded and named as excluded**, with any `reconnect_url` surfaced. Then settle the entity scenario — single-entity: which company; multi-entity: which set — always by `display_name`. |
| 3 | **Which tools do those sources actually expose?** | `get_datasource_tools` per in-scope datasource, **and per company file when several are live — permissions are per company.** This is discovery, not a question: read what is there before promising anything. |

**Never auto-pick.** Not the workspace, not the company file, not the entity scenario.
**Silence is not a yes**, and an answer to one question is not an answer to the next.

**Names, never internal ids.** Name the workspace and refer to companies by
`display_name`. Never print an internal numeric tenant id, and never show a raw
`data_source_id` — pass the opaque handle, show the name.

**Only then does the work begin.** Once the workspace, the data sources and their tools
are confirmed, resolve every task against what was actually found: what is available
now decides which steps are `[auto]`, which are `[gated]` and which fall to `[manual]`.
Where the confirmed tools cannot answer the request, say so and ask — do not substitute
an assumption for a capability.

**The catalogue is authoritative.** Take exact `tool_name` values from the Gate 2
listing — names are not uniformly styled, some underscored, some hyphenated.
**Do not invent a tool name.** On `UNKNOWN_TOOL`, read the valid names from the error
and retry.
**Never call a tool whose `effective_policy` is `disabled`.**

**This map is built fresh every run** and held only for this run. It is written out in
the coverage sheet, never written back into this file.
<!-- shared:scope-protocol end -->

## Gate 0 — Confirm the workspace

Call `list_workspaces` with **no arguments**.

- **One workspace** → read the workspace **name** back and **wait for an explicit yes**.
- **Two or more** (`selection_required`) → ask **in chat** whether this is single- or
  multi-workspace, then which workspace(s) **by name**, then call again with `workspace_ids=[…]`
  and `mode="single"` / `mode="multi"`.

Never auto-pick. Never print an internal numeric tenant id — name the workspace, pass the opaque
`ws_…` handle.

## Gate 1 — Discover live datasources and settle the entity scenario

Call `get_agent_datasources` with the confirmed `workspace_id`.

- `connected: true` → **in scope**.
- `connected: false` → **excluded, and named as excluded**. Surface any `reconnect_url`.

**Check specifically for a connected T&E or spend-management platform** — Concur, Expensify, Brex, Ramp,
Pleo, Spendesk, Soldo. **If one is live, the expense report itself becomes `[auto]`**, which converts the
skill's largest manual input. If none is, the report is supplied by hand and labelled as such. Either way
the accounting datasource still supplies the COA, the employees and the posted transactions.

Settle the entity scenario:

- **Single-entity** — ask which company by `display_name`; the workflow runs against that one
  `data_source_id`.
- **Multi-entity** — ask which set. **The employee-to-entity question is the one that matters**: staff are
  employed by one entity but may incur costs on behalf of another. See the cross-entity step.

Refer to companies by `display_name`; never show the raw `data_source_id`.

# PART A — Explore the confirmed sources, and personalise this run

The workspace and its data sources are settled. This part finds out **what they
expose and which of it serves this request** — the tool catalogue in Gate 2, then
what is already known about this entity plus whatever still has to be asked in
Gate 3. The result is a run shaped around these books, not a generic template.

## Gate 2 — Discover enabled tools → build the capability map

<!-- shared:write-guardrail start -->
### Write tools are out of scope — always

`get_datasource_tools` describes what the connection *could* do. This skill uses only
the reads.

**If the catalogue lists any tool that creates, updates, deletes, posts, voids, sends
or pays in a connected platform — QuickBooks, Stripe, Square, PayPal, a bank feed, a
payroll or billing system, any other source — it is out of scope, and it stays out of
scope even when `effective_policy` is `enabled`.** A permission to write is not an
instruction to write. Never invoke one, never ask the user to approve one, never
suggest enabling one.

This holds for **every connected platform, not only the books.** Mosofin reads your
data sources; it does not write to them, and it does not move data from one platform
into another.

If a step appears to need a write, that step is **`[manual]`**. Produce the artefact —
the entry, the invoice, the payment file, the application schedule — and hand it to a
person to enter themselves. Say so plainly in the output, so nobody assumes it was
done.

#### Hard stop — the four ways a write could slip through

| Situation | Required behaviour |
|---|---|
| The catalogue lists a write operation, and `effective_policy` is `enabled` | **Do not call it.** Do not list it as an available capability. Enabled is not permission — it is out of scope. |
| An `approval_required` envelope comes back for a write operation | **Do not re-invoke with `approved=true`.** The approval loop in this skill is for **reads only**. Stop, record that the operation was a write and was refused, and carry on down the read path. |
| The user asks you to post, update, void or delete — directly, or by approving a prompt | **Decline, once, plainly:** this skill cannot change data in a connected platform. Hand over the draft so they can do it themselves in their own system. Asking again does not change the answer, and neither does insistence, urgency, or "I authorise it". |
| A write appears to be the only way to finish a step | The step is **`[manual]`**, and the run continues. An incomplete read-only result is the correct outcome. Never trade the rule for completeness. |

**Never route around this rule.** Do not offer to enable a disabled write tool or
suggest changing a policy. Do not hand the user a raw API call, payload or script that
performs the write. Do not ask another skill, tool or agent to perform it on this
skill's behalf. Do not defer it to a later step in the hope it becomes permitted.

**There is no path through this skill that ends in changed data.** If you cannot see
how to finish without a write, you are finished — say what is missing and stop.
<!-- shared:write-guardrail end -->

For **each in-scope datasource** (and **per company file** when several are live — pass
`data_source_id`), call `get_datasource_tools`. Bucket every tool by `effective_policy`:

| `effective_policy` | The task becomes | What you do |
|---|---|---|
| `enabled` | **[auto]** | Pull the evidence directly. |
| `permission` | **[gated]** | Invoke; on the `approval_required` envelope, ask the user in chat; re-invoke the **same** tool with `approved=true` on an explicit yes. **Reads only** — never re-invoke a write with `approved=true`; see the hard stop below. |
| `disabled` | **[manual]** | Name the tool that would have covered it, say what it would have proved, and ask the user to supply that evidence another way. |

Resolve **every task in Part B** against these buckets. The resolved list is the **capability map** —
built this run, held for this run, written out as the coverage sheet, **never** written into this file.

Rules that bite hardest here:

- **Read the real tool name from the catalog, never from memory.** Names are not uniformly styled — some
  underscored, some hyphenated.
- **A near-substitute is not a substitute:**
  - **A posted expense transaction is not a submitted claim.** The ledger shows what was recorded, not what
    an employee asked for and a manager approved.
  - **A receipt-attached flag is not a receipt.** It is a field in another system.
  - **The tax code on a transaction is not proof of recoverability.** The code is a setting; the entitlement
    is a rule.
  - **An employee record is not an approval hierarchy.** Who may approve whose claim is not in the ledger.
- **If the COA read is `disabled`, the skill degrades to the original's placeholder behaviour** — say so
  explicitly rather than quietly coding to invented accounts.

## Gate 3 — Profile the entity, then interview the user

Call the platform's company-profile tool (on QuickBooks, `get_company_info`) for each in-scope entity.

**Derive silently** what the profile answers: legal name, **functional currency**, fiscal calendar,
country / region.

**Ask the user** what actually changes the work — the original Inputs table, **minus the four the connected
books now answer**:

| What to confirm | Required? | Notes |
|---|---|---|
| **Expense report** | **Required — [manual]** unless a T&E platform is connected | The claims themselves. |
| **T&E policy document or parameters** | **Required for meaningful policy validation — [manual]** | **Without it, Step 2 does not run.** Say so rather than inventing limits. |
| **Receipts** | Recommended — **[manual]** | Images are not on this surface. |
| ~~Employee details~~ | **Now [auto]** | From `search_employees`; confirm only if a claimant is not found. |
| ~~Chart of accounts~~ | **Now [auto]** | From `search_accounts`. **No placeholder coding.** |
| **Tax jurisdiction** | Recommended — **[manual]** for the rules | The **codes in use** are `[auto]`; the entitlement rules are not. |
| **Mileage rate** (per-km or per-mi) | Required if mileage claims present — **[manual]** | **Never supply a prevailing rate from memory.** |
| **Per-diem table** | Required if per-diems are claimed — **[manual]** | Same rule. |
| ~~Functional currency~~ | **Now [auto]** | From the company profile. |
| **FX rate source** | Required if multi-currency — **[manual]** | Central bank close, accounting system rate, or report-date rate. **Do not assume a source.** |
| **Confirm scope** | **Required** | Read back in-scope and excluded company files by `display_name`, and the report period. |
| **Confirm any profile contradiction** | **Required if one appears** | e.g. claims in a currency the entity does not transact in. |
| **Confirm manual evidence** | **Required** | The policy, the rates and the receipts are `[manual]`. Record what was supplied and as at when. |

Ask as **one short batch**. Propose defaults where reasonable — but **never** default a mileage rate, a
per-diem, a receipt threshold, a deductibility split, a gift cap, or an FX source.

**On later runs**, read stored preferences first (Step 7), confirm in one line, and ask only what changed.
The policy parameters, the category-to-account map, the rates in force and the FX source persist; **the
claims and the balances are re-obtained every run**.

---

# PART B — The domain work

Every step below is the original procedure, unchanged in count, order, or substance, with plain-language
wording, an `[auto]` / `[gated]` / `[manual]` verdict, and the typical evidence tool added.

**Never drop a task because no tool covers it.** Policy validation without a policy is the original's own
documented limit and remains so.

Tool names in *italics* are typical. Resolve real names and policies from your Gate 2 catalog.

## Step 0 — Fetch the evidence (grounding) — Mosofin addition

Pull the `[auto]` / `[gated]` reads **before** parsing, because the COA and the posted transactions are
what make the coding and the duplicate check real rather than generic.

**Batch independent reads into one message** — the accounts, the employees, the posted expenses and the
balances do not depend on each other. Never serialize them.

The server is **stateless**: pass `data_source_id` on **every** call, including retries.

Typical opening batch, per in-scope entity:

- *`search_accounts`* — **the real chart of accounts**, including the travel, meals, entertainment, gift
  and clearing accounts — usually **[auto]**
- *`search_employees`* — **the claimant list**, with departments where set up — usually **[auto]**
- *`search_purchases`* / *`search_expenses`* / *`search_bills`* — **transactions already posted in the
  period**, which is the duplicate-check population — usually **[auto]**
- *`search_credit_card_charges`* or the card account in *`get_general_ledger`* — **corporate card
  activity** — usually **[auto]**
- *`get_general_ledger`* on **Corporate Card Clearing** and **Employee Reimbursement Payable** — usually
  **[auto]**
- *`get_balance_sheet`* — the **payable and Due-from-Employee balances** — usually **[auto]**
- *`search_vendors`* — merchant recognition — usually **[auto]**
- *`get_profit_and_loss`* by department / class — **historical coding patterns** — usually **[auto]**
- *`search_departments`* / *`search_classes`* — the dimensions available for allocation — usually **[auto]**
- *`get_company_info`* — legal name, **functional currency**, country — usually **[auto]**

Handle the envelopes:

- `approval_required` → ask the user in chat, then re-invoke the same tool with `approved=true`.
- `entity_required` → ask by `display_name`, then pass that `data_source_id`.
- `tool_policy_disabled` → convert that task to **[manual]** and record the gap. **If it is the COA read,
  say explicitly that coding has degraded to placeholders.**
- `UNKNOWN_TOOL` → read the valid names from the error; do not guess.
- Dead connection → surface the `reconnect_url`.

Check the **`mock` flag**. `mock: true` is fixture data — **a duplicate check against fixture transactions
proves nothing**, and must not be reported as a clean result.

## Step 1 — Parse the report

Extract per line — **[manual]** unless a T&E platform is connected:

- Employee name
- Expense date
- Category (Airfare, Lodging, Meals, Ground Transport, Mileage, Supplies, Other)
- Vendor / merchant
- Description / business purpose
- Amount (gross; **tax and tip separately if shown**)
- Currency
- Payment method (employee cash / card, corporate card, reimbursement requested?)
- Receipt attached: Y/N
- Reference number / unique line ID

**Tool-specific normalization tips:**

- **Concur**: handles **split transactions across categories — preserve splits, don't merge.**
- **Expensify**: **SmartScan confidence — flag low-confidence OCR'd lines.**
- **Brex / Ramp / Pleo / Spendesk**: **corporate card transactions already paid — distinguish from
  out-of-pocket.**
- **Soldo / Mooncard / Pleo**: **prepaid card transactions — treat as company-paid.**

That distinction is now load-bearing: **company-paid lines are the ones that should already exist in the
ledger**, and Step 2's duplicate check depends on classifying them correctly.

**Normalize messy data:**

- **Mileage entries** → capture **distance and unit (mi or km), and rate (or flag as missing)**
- **Foreign currency** → **preserve original**; use the provided FX rate or **flag for the user to supply**
- **Tip separately captured if shown**

**Mosofin addition — match each claimant to an employee record.** **[auto]** via *`search_employees`*. A
claimant with no employee record is worth flagging: a leaver, a contractor claiming on an employee form, or
a misspelling.

## Step 2 — Validate against policy

**Policy validation only runs if the user provided a policy.** If no policy, **skip this step and move to
Step 3** — GL coding, math check and obvious-personal flags only. **[manual]** on the policy throughout;
the arithmetic against it is `[auto]`.

When a policy is provided, validate per its rules. Common dimensions:

### Receipt requirements

Apply **the user's** policy thresholds. Common patterns to look for in the policy:

- **A minimum amount above which receipts are required**
- **Specific categories that always require receipts regardless of amount** — commonly lodging, client
  entertainment, anything reimbursable cross-border
- **Tax-authority requirements for the entity's jurisdiction** — if the policy doesn't address this, **flag
  for the user to confirm**

**Flag missing-receipt items per the policy's stated rules.** **[gated]**: the threshold comparison is
arithmetic, but **the receipt flag comes from the T&E tool and the image is not visible here.** Report it as
"per the report", not as verified.

### Per-diem

If the entity uses per-diems for lodging and/or meals — **[manual]** table, `[auto]` comparison:

- **Compare claimed amount to the per-diem table provided**
- **Flag any claim exceeding the per-diem**
- **For mixed regimes** (per-diem M&IE + actual lodging) — **flag meal receipts as potential duplicate
  claims**

### Meals & entertainment

- **Per-person caps from policy**
- **Alcohol allowed / restricted per policy**
- **Client meals: require attendees + business purpose per policy**
- **Deductibility treatment per jurisdiction** (if the user provided the jurisdiction): some jurisdictions
  allow full deduction, some partial, some none — **split the GL coding accordingly only if the user
  provided the rule**

**[auto]** contribution: whether the COA *has* separate deductible and non-deductible accounts is readable
(*`search_accounts`*). If it does not, that is the Step 3 flag, raised early.

### Airfare / rail / sea travel

- **Class restrictions per policy** (economy, premium economy, business)
- **Booking source restrictions**
- **Personal segment allocation if the trip extends personal days**

### Lodging

- **Per-night caps from policy**
- **Resort / cleaning / incidental fees — reimbursable per policy**
- **Personal nights — non-reimbursable**

### Ground transport

- **Ride-share, taxi, rental car — per policy**
- **Mileage at the rate provided by the user / policy / jurisdiction's prevailing rate** — **[manual]**,
  and **never supplied from memory**

### Entertainment

- **Deductibility flag per the entity's jurisdiction** (only if the user provided it)
- **Per policy: allowed / restricted / requires pre-approval**

### Gifts

- **Cap per recipient per policy**
- **Deductibility cap per jurisdiction** (only if the user provided the rule)

### Personal-expense patterns (red flags)

**These are universal red flags regardless of jurisdiction** — and they are pattern checks, so `[auto]`
against the parsed report:

- **Weekend charges in non-business locations**
- **Personal retail / grocery / liquor / personal services**
- **Family-member-named travel or accommodation**
- **Recurring personal subscriptions**

**Flag as "Policy violation — possible personal" for manager review.** *Mosofin note*: a recurring personal
subscription often also appears in the ledger month after month (*`get_general_ledger`*), which turns a
suspicion into a documented pattern with a start date.

### Duplicate detection

- **Same vendor + amount + date submitted twice by the same employee** — `[auto]` within the report
- **Same expense claimed on both employee report and corporate-card reconciliation** — **`[auto]` against
  the ledger, and this is the check the original could only describe.** Match each claim line against
  posted card transactions on vendor, amount and a date window. **A claim for a charge the company already
  paid is a double reimbursement**, and it is the highest-value finding this skill produces.

**Match tolerances**: exact amount and same merchant is a strong match; allow a **±3 day** window for
posting lag, and treat a near-amount match (tip added later) as a **review**, not a reject. Where the
merchant name differs between the card feed and the claim, fall back to amount plus date — and say the
match was made on those alone.

See `duplicate-invoice-detection` for the full matching discipline; the same reasoning applies.

## Step 3 — Code to GL accounts

**Hand off the coding logic to the `gl-coding-assistant` skill's conceptual category mappings.** **Account
numbers come from the user's COA only. Never invent account numbers.** **[auto]** here — the COA is
connected.

Common T&E category → conceptual GL category (mapped to the user's COA):

- **Airfare** → Travel — Airfare
- **Lodging** → Travel — Lodging
- **Meals (business, deductible portion)** → Meals (deductible)
- **Meals (non-deductible portion, per jurisdiction)** → Meals (non-deductible)
- **Ground transport / mileage** → Travel — Local Transport
- **Conferences / training** → Training & Development
- **Office supplies** → Office Supplies
- **Software / subscriptions** → Software Subscriptions
- **Client gifts (deductible portion)** → Client Gifts
- **Client gifts (non-deductible portion)** → Non-deductible / disallowed
- **Entertainment (deductible per jurisdiction)** → Entertainment
- **Entertainment (non-deductible per jurisdiction)** → Entertainment — Non-Deductible
- **Communications** → Telephone & Internet
- **Miscellaneous** → Other — **flag for review**

**If the user's COA does not separately track deductible vs. non-deductible portions, flag and suggest
creating sub-accounts or using a class / department dimension.** **[auto]** to determine — read the COA and
the available dimensions and say which is actually available in this entity.

**Mosofin addition — ground each mapping in history.** **[auto]**: before proposing an account, check how
this entity has coded the same category, and the same merchant, previously (*`get_general_ledger`*,
*`get_profit_and_loss`*). **Report the suggestion with its support** — "Travel — Airfare (6410); this
merchant coded there 12 of 12 times" — and **flag where history is inconsistent**, because an inconsistent
history is itself worth knowing before the reviewer perpetuates it.

## Step 4 — Calculate reimbursement

For each employee:

| Component | Calculation |
|-----------|-------------|
| **Total claimed** | Sum of all approved lines |
| **Less: Corporate-card-paid items** | Reduce reimbursement |
| **Less: Policy violations / disallowed** | Removed |
| **Less: Personal portion** | Allocated to employee |
| **Plus: Mileage at provided rate** | Distance × rate |
| **Net reimbursable to employee** | Sum |

**[auto]** arithmetic; the rate and the policy calls behind it are `[manual]`.

**Multi-currency: convert to functional currency using a stated FX source** — ask the user: **central bank
close, accounting system rate, or report-date rate from a stated provider. Do not assume a source.** The
**functional currency itself is now `[auto]`** from the profile; the *rate source* is not.

**Mosofin addition — reconcile to the payable.** **[auto]**: the proposed net reimbursable, added to the
existing **Employee Reimbursement Payable** balance, should explain that account's movement once paid.
**A payable balance that does not correspond to outstanding claims means either a claim was paid twice or
one was never paid** — both worth surfacing.

## Step 5 — Construct the GL posting

**Hand off to the `journal-entry-builder` skill.** Provide it with:

- **Posting date**
- **Employee name and period**
- **Sum of expenses by GL account** (using the user's COA — **`[auto]`, real accounts**)
- **Sum of input tax recoverable** (if the jurisdiction supports it and the user has provided the rule)
- **Reimbursable amount** → **AP / Employee Reimbursement Payable**
- **Corporate-card-paid amount** → **Corporate Card Clearing**

**The JE skill produces the balanced entry. This skill provides the inputs.** **Mosofin does not post** —
the entry is a proposal, whichever skill assembles it.

**Mosofin addition — the clearing account test.** **[auto]**: **Corporate Card Clearing should clear.**
Read its balance and its ageing (*`get_general_ledger`*). A persistent residual means card charges posted
with no matching claim, or claims posted with no matching charge — the two halves that were supposed to
meet. **Report the residual with its oldest item**, because age is what tells you whether it is timing or a
genuine break.

## Step 6 — Output

Deliver an `.xlsx` workpaper:

**Sheet 1: Summary**
- **Employee(s), report period**
- **Total claimed / approved / rejected**
- **Reimbursement amount**
- **Policy violations summary**
- **Policy assumptions used** — if any were not explicit in the policy doc, **list them here for the user to
  confirm**

Plus the Mosofin header block: workspace name; the entity by `display_name`; each excluded company file and
why; whether any figure rests on `mock` data; **and whether a policy was supplied at all**.

**Sheet 2: Line Detail**

| Line # | Date | Category | Vendor | Description | Amount | Currency | FX Rate | Functional Amount | GL Account | Tax Code | Policy Check | Receipt? | Notes |

Add two Mosofin columns: **Alrea

…(truncated)
