Spreadsheets to Approval Workflows: From a Request Sheet to an Approval and Audit Record
Requests collected in a sheet can be checked, routed and recorded by fixed rules, and you can read every decision afterwards. This page takes one small synthetic request sheet and one decision sheet through four ordered steps, shows each outcome and audit record, and states what the record does not prove.
A spreadsheet approval workflow reads each request row, rejects rows with bad data, and routes the valid ones by amount to a proposed action or to a named approver role. Decisions are checked against a roster, and every step is written to a hash-chained audit record that shows who did what, and when.
On this page
- What goes in: the request sheet and the decision sheet
- Validation: which rows are rejected for bad data?
- Thresholds: how do approval thresholds route a row?
- The result on the synthetic fixture
- What happens at the approval step, and what is recorded?
- When should you not use this approach?
- Frequently asked questions
- Related reading
- Sources
At a glance
- A request sheet exported as CSV can be validated, routed by amount to an approver role and recorded, with every step written to an audit trail.
- A row with bad data is rejected with its sheet row number and never reaches an approver.
- An approval from someone who is unknown, is the requester, or holds too low a role is refused and logged, and the request stays pending.
- A proposal is a record only; the module pays and posts nothing.
- The demonstration is synthetic and shows the method, not a connector or real-volume performance.
What goes in: the request sheet and the decision sheet
The workbook is one sheet of a spreadsheet workbook exported as CSV. RFC 4180 notes that "there is no formal specification in existence" for the CSV format.[1] The module reads a request sheet and a decision sheet, finds columns by header name, and holds money as integer cents so rounding never decides a threshold. For CSV parsing and matching, see CSV ledger reconciliation.
| Sheet | Columns |
|---|---|
| Request sheet | request_id, submitted_date, requested_by, vendor, cost_center, amount, description (optional) |
| Decision sheet | request_id, approver, decision, decided_at |
| Parameter | Value |
|---|---|
| Auto limit | 1000.00 |
| Director limit | 10000.00 |
| Cost centers | CC-100, CC-200, CC-300 |
| Approver roster | m.ortiz = cost_center_manager, d.chen = finance_director |
| Finance director | May approve either tier |
Illustrative example (synthetic data, not client data or real payments): the next two tables are the complete input. The request table omits the optional description column and joins cost center and amount in one column.
| request_id | submitted_date | requested_by | vendor | cost_center / amount |
|---|---|---|---|---|
| R001 | 2026-03-02 | a.park | Alder Office Supply | CC-100 / 420.00 |
| R002 | 2026-03-02 | a.park | Birch Catering | CC-100 / 1000.00 |
| R003 | 2026-03-03 | j.lee | Cedar Cleaning | CC-200 / 1000.01 |
| R004 | 2026-03-03 | j.lee | Dune Software | CC-200 / 2750.00 |
| R005 | 2026-03-04 | t.nguyen | Elm Equipment | CC-300 / 18400.00 |
| R006 | 2026-03-04 | a.park | Fir Courier | CC-100 / 640.00 |
| R007 | 2026-03-05 | a.park | Fir Courier | CC-100 / 640.00 |
| R008 | 2026-03-05 | m.ortiz | Gum Consulting | CC-200 / 6200.00 |
| R009 | 2026-03-06 | j.lee | Holly Print | CC-200 / 12.345 |
| R010 | 03/06/2026 | t.nguyen | Ivy Signage | CC-300 / 980.00 |
| R011 | 2026-03-09 | a.park | Juniper Training | CC-999 / 1500.00 |
| R003 | 2026-03-09 | j.lee | Kelp Catering | CC-200 / 350.00 |
| request_id | approver | decision | decided_at |
|---|---|---|---|
| R003 | m.ortiz | approve | 2026-03-04T10:15:00Z |
| R004 | m.ortiz | decline | 2026-03-04T11:40:00Z |
| R005 | m.ortiz | approve | 2026-03-05T09:05:00Z |
| R005 | d.chen | approve | 2026-03-05T14:30:00Z |
| R008 | m.ortiz | approve | 2026-03-06T08:50:00Z |
Validation: which rows are rejected for bad data?
Before a request is proposed, check that request_id, requested_by and vendor are present (description is optional), its request id is not already used, its date is a real YYYY-MM-DD date, its amount is a positive decimal with up to two places, and its cost center is on the policy list. A row that fails is rejected with its sheet row number and logged. It never reaches an approver.
| Sheet row | request_id | Code | What was wrong |
|---|---|---|---|
| 10 | R009 | bad_amount | 12.345 has three decimal places |
| 11 | R010 | bad_date | 03/06/2026 is not YYYY-MM-DD |
| 12 | R011 | unknown_cost_center | CC-999 is not on the list |
| 13 | R003 | duplicate_request_id | R003 was already used above |
The sheet row number counts the header as row 1, so the first data row is row 2; this assumes no blank rows. Rejecting at the door keeps a wrong amount or unknown cost center from being routed on bad data.
Thresholds: how do approval thresholds route a row?
An approval threshold is an amount above which a request needs a named approver role. The module applies the policy parameters above, with the limit itself passing, and it keeps the steps in a fixed order.
- Validate each row
Reject bad data with its sheet row number and log it.
- Propose from the thresholds
An amount at or under the auto limit is proposed with no approver. Over it, the request is pending approval by the cost center manager, or by the finance director above the director limit. At or under the auto limit, the same vendor and amount as an earlier valid row also goes to approval, as a possible duplicate.
- Apply decisions in file order
Refuse and log a decision for an unknown request id, a request that is not pending approval, an unknown approver, the requester, or a role that is too low. A malformed decision row stops the run.
- Append an audit record at every step
Each record states who, what, when, the state before and the state after.
| Condition | Outcome | Approver role |
|---|---|---|
| Amount at or under 1000.00 | proposed | none |
| Over 1000.00, up to 10000.00 | pending_approval | cost_center_manager |
| Over 10000.00 | pending_approval | finance_director |
| Same vendor and amount as an earlier valid row, at or under 1000.00 | pending_approval (possible duplicate) | cost_center_manager |
The result on the synthetic fixture
Illustrative example (synthetic data): of the 12 rows, 8 are valid and 4 are rejected. Once decisions are applied, 3 are proposed, 2 approved, 1 declined and 2 still pending approval. These counts describe the fixture only, not a typical rate.
| request_id | amount | Outcome | Why |
|---|---|---|---|
| R001 | 420.00 | proposed | Within the auto limit; no approver. |
| R002 | 1000.00 | proposed | The limit itself passes. |
| R003 | 1000.01 | approved | Just over the limit; approved by m.ortiz as manager. |
| R004 | 2750.00 | declined | Declined by m.ortiz. |
| R005 | 18400.00 | approved | Over the director limit; m.ortiz's approval refused for role, then approved by d.chen. |
| R006 | 640.00 | proposed | Within the auto limit; no approver. |
| R007 | 640.00 | pending_approval | Possible duplicate of R006; still pending. |
| R008 | 6200.00 | pending_approval | Requested by m.ortiz, whose approval was refused as the requester; still pending. |
Illustrative example (synthetic data): raising the auto limit to 3000.00 moves R003 and R004 to proposed with no approver. R008 at 6200.00 still needs approval. A threshold is a policy choice, and moving it changes who sees a request.
What happens at the approval step, and what is recorded?
When an approver is not allowed to approve, the module refuses the decision, writes a refusal to the audit record and leaves the request pending. In the synthetic fixture, m.ortiz's approval of R005 is refused because a cost center manager cannot approve a finance-director request. m.ortiz's approval of R008 is refused because m.ortiz is the requester. This is one separation-of-duties rule: the person who requests a payment cannot approve it.
An audit record states who acted, what they did, when, and the state before and after. Here each record has the fields seq, at, actor, action, subject, before, after and detail, plus prevHash and hash. Illustrative example (synthetic data): the run produces 17 records, namely 4 row_rejected, 8 action_proposed, 3 approval_recorded and 2 approval_refused.
| seq | actor | action | subject | Change and detail |
|---|---|---|---|---|
| 1 | system:validator | row_rejected | row:10 | received to rejected_bad_data; bad_amount: amount "12.345" must be a positive decimal with up to two places |
| 9 | system:policy | action_proposed | R005 | received to pending_approval; over_director_limit; finance_director; limit 10000.00 |
| 12 | system:policy | action_proposed | R008 | received to pending_approval; over_auto_limit; cost_center_manager; limit 1000.00 |
| 15 | m.ortiz | approval_refused | R005 | pending_approval to pending_approval; role_too_low; needs finance_director |
| 16 | d.chen | approval_recorded | R005 | pending_approval to approved; approve as finance_director |
| 17 | m.ortiz | approval_refused | R008 | pending_approval to pending_approval; approver_is_requester |
The excerpt omits the at column. System records use a fixed run time of 2026-03-10T09:00:00Z, which is a parameter of the run. Decision records use the decided_at time from the decision sheet. Each record carries the hash of the one before it, which forms a hash chain: a change to an earlier record no longer matches the hash stored in the next, so edits are detectable. The chain does not make edits impossible.
A tamper check recomputes the chain. In the synthetic run the chain verifies. Editing one record's after value, or removing record 5, makes verification fail. The limit is real: removing the last records leaves a valid shorter chain, and anyone can recompute an unkeyed SHA-256 chain, so it is conclusive only against a copy of the last hash kept elsewhere. The CSV export omits prevHash and hash, so a file alone cannot be verified. It is not a substitute for access control or a retention policy.
When should you not use this approach?
Do not automate when manual routing already keeps up, when limits and roles are not agreed, or when the payment is high-risk.
This demonstration shows how rows become records, not how it performs on your data. A spreadsheet's own history is one way to see changes: Microsoft Support says its feature lets you "see who changed a workbook, what they changed, and when they made the change," and that "You can view changes from the past 365 days."[2] An audit record you keep yourself is not bounded by that product window.
- All data is synthetic and small. Nothing here was tested on real requests or at real volumes.
- It is a method over two CSV files, not a connector to any product, and it pays, posts or releases nothing.
- An audit record shows what the module was told and did. It does not prove a request was legitimate, that the approver was who they claimed, or that the money went to the right place.
- The roster, limits and cost centers are inputs. If they are wrong or stale, the routing is wrong.
- The approver and time of each decision are copied from the decision sheet and are not authenticated.
- Do not use it as the only control for high-risk payments. Keep approval in the system of record; see which finance actions need which approval.
- A sheet that needs concurrent editing, attachments or many columns of status has been outgrown; see when a tracking spreadsheet stops being enough.
Frequently asked questions
How do you turn a spreadsheet of requests into an approval workflow?
Export the sheet as CSV, validate each row, then route valid rows by amount to a proposed action or an approver role. Apply decisions in file order, refuse any that are not allowed, and write an audit record at every step. Rows over the auto limit wait for a person's approval.
What should be validated before a request row is proposed?
Check that request_id, requested_by and vendor are present (description is optional), the request id is unused, the date is a real YYYY-MM-DD date, the amount is a positive decimal with up to two places, and the cost center is on the policy list. A failing row is rejected with its sheet row number.
How do approval thresholds route requests?
An amount at or under the auto limit is proposed with no approver. Above it, the request goes to a cost center manager, or to a finance director above the director limit. At or under the auto limit, a repeat of an earlier vendor and amount is also sent to approval as a possible duplicate.
What should an audit record contain?
Who acted, what they did, when, which request, and the state before and after. This demonstration also chains each record to the previous one by hash, so an edited record, or a removed record other than the last, fails verification unless every later hash is recomputed. The check is conclusive only against a copy of the last hash kept elsewhere.
What happens when an approver is not allowed to approve?
The decision is refused and logged as a refusal with its reason, and the request stays in its earlier state. Reasons include an unknown request id, a request that is not pending approval, an unknown approver, the requester, and a role too low. A malformed decision row stops the run.
When should you not automate approvals from a spreadsheet?
Skip it when volume is low enough that manual routing keeps up, when limits and roles are not yet agreed, or when the payment is high-risk and needs the system of record's own controls. An audit record does not prove a request was legitimate.
Related reading
- The wider legacy finance workflow automation approach covers working through older systems' existing interfaces.
- Which finance actions need which approval is the role and action matrix; this page is the worked example of one gate.
- When a tracking spreadsheet stops being enough covers a different workflow that outgrew its sheet.
See where AI could take the most manual work off your plate.
A 30-minute strategy call. We'll map your highest-value automation candidates and what a production system would return.