{
  "jimothyPipeline": 1,
  "name": "Card statement reconciliation",
  "description": "Matches card charges to submitted expenses and sends each cardholder their monthly statement with the charges still to clear.",
  "icon": "workflow",
  "color": "#059669",
  "repo": {
    "mode": "inplace",
    "localPath": ""
  },
  "concurrency": 1,
  "variables": {
    "company": "Acme Inc.",
    "cardholders": "Dana Lee, dana@acme.example\nSam Ortiz, sam@acme.example",
    "dateWindowDays": "3",
    "replyByDays": "5",
    "sendWebhookUrl": ""
  },
  "steps": [
    {
      "id": "find",
      "name": "Find exports",
      "type": "shell",
      "description": "Picks the newest statement export and the newest expense export in the folder.",
      "timeoutMinutes": 1,
      "command": "s=$(ls -t -- *statement*.csv 2>/dev/null | head -1); [ -n \"$s\" ] || { echo \"Put this month's card statement export (a .csv with \\\"statement\\\" in its name) in $(pwd)\"; exit 1; }; e=$(ls -t -- *expense*.csv 2>/dev/null | head -1); echo \"Statement: $s\"; echo \"Expenses: ${e:-(none)}\""
    },
    {
      "id": "match",
      "name": "Match charges",
      "type": "agent",
      "description": "Matches every card charge to a submitted expense with a script and drafts one statement per cardholder.",
      "harnessId": "claude-code",
      "model": "sonnet",
      "dependsOn": [
        "find"
      ],
      "timeoutMinutes": 30,
      "prompt": "You are reconciling this month's company card statement for {{vars.company}}. The exports in the current folder are:\n{{steps.find.output}}\n\nCardholders (name on the statement, email):\n{{vars.cardholders}}\n\n1. Inspect the files to learn their columns. Write and run a script (Python or awk) that does all of the matching and arithmetic. Never add up amounts in your head; every number you report must come from the script's output.\n2. Match each card charge to at most one submitted expense from the same person where the amounts are equal to the cent and the dates are at most {{vars.dateWindowDays}} days apart. Prefer the closest date, then the most similar merchant name. Refunds and credits are charges too.\n3. For each cardholder, compute: number of charges, total spend, matched total and unmatched total. Also list expenses submitted as paid by card that have no charge on the statement.\n4. Save every charge with its match status (matched, with the expense it matched, or unmatched) to a new file card-reconciliation-<statement period>.csv. Do not modify or delete any other file.\n\nYour final answer is Markdown in exactly this shape:\n## Checks\nStatement total, the sum of the cardholder totals (they must be equal), number of charges, number matched, and any cardholders on the statement who are not in the cardholder list.\n\n### Statement 1: <Cardholder> · <total spend> · <n> unmatched\nTo: <email from the cardholder list, or \"(not in cardholder list)\">\nSubject: Your {{vars.company}} card statement for <period>\n\n<email body>\n\nOne \"### Statement\" section per cardholder, most unmatched charges first, and nothing after the last one. Each email is plain text: a one-line summary of their spend, then a table of the unmatched charges (date, merchant, amount) asking them to submit each as an expense with a receipt within {{vars.replyByDays}} days, or to reply straight away if they don't recognise a charge. Then list their matched charges briefly so they can see everything on their card. If nothing is unmatched, say so and thank them. Sign it \"Finance, {{vars.company}}\".\n{{#if steps.review.output}}\n\nA reviewer checked your previous answer. Fix every problem it lists:\n{{steps.review.output}}{{/if}}{{#if steps.pick.output}}\n\nYour previous draft was rejected with this feedback. Rewrite it:\n{{steps.pick.output}}{{/if}}"
    },
    {
      "id": "review",
      "name": "Check the totals",
      "type": "agent",
      "description": "A second model checks every statement adds up and ties back to the card statement total.",
      "harnessId": "anthropic-api",
      "model": "claude-opus-5",
      "dependsOn": [
        "match"
      ],
      "timeoutMinutes": 10,
      "passPattern": "VERDICT:\\s*APPROVE",
      "loopBackTo": "match",
      "maxLoops": 2,
      "prompt": "Review this card reconciliation before the statements go out to cardholders.\n\n{{steps.match.output}}\n\nCheck that:\n- the statement total in Checks equals the sum of the cardholder totals;\n- in each statement, the unmatched count in the heading equals the rows in its unmatched table, and the matched and unmatched charges add up to its total spend;\n- each email only contains that cardholder's own charges;\n- every statement has a To: address, or says the person is not in the cardholder list.\n\nStart with a three-line summary for the finance team: total spend, number of charges still unmatched, and anything that needs their attention before sending. Then list each problem you find with the cardholder it affects. End with exactly one line: VERDICT: APPROVE if there are no problems, otherwise VERDICT: CHANGES_REQUESTED."
    },
    {
      "id": "pick",
      "name": "Pick statements to send",
      "type": "approval",
      "description": "You tick the statements to send. Reject with a comment and they are redrafted with your notes.",
      "dependsOn": [
        "review"
      ],
      "loopBackTo": "match",
      "maxLoops": 3,
      "choices": {
        "fromStep": "match",
        "mode": "multiple"
      },
      "approvalMessage": "Pick the statements to send. Leave out anyone not in the cardholder list, or reject with a comment to have them redrafted.\n\n{{steps.review.output}}"
    },
    {
      "id": "send",
      "name": "Send statements",
      "type": "shell",
      "description": "Sends the picked statements to your finance inbox through a webhook.",
      "dependsOn": [
        "pick"
      ],
      "timeoutMinutes": 2,
      "command": "printf '{\"statements\":%s}' {{steps.pick.selected | json}} | curl -fsS -X POST -H 'Content-Type: application/json' --data @- {{vars.sendWebhookUrl}}"
    }
  ]
}