Skip to content
All builds
Operations

SluiceForm to Sheet Pipeline

What it does for you

Every form submission lands in one clean spreadsheet.

Form submissions from five tools mapped to one schema, cleaned, deduped with an idempotency key, and appended to a sheet, with a dead letter you can fix and retry and a hand-built .xlsx export.

Sluice screenshot

Who it's for

Small businesses that collect forms in more than one place, like Typeform, Google Forms, Jotform, Tally, or their own site. Duplicates get skipped, and anything that can't go in waits in a list with the reason.

Screenshots

Sluice screenshot 1

In more detail

Sluice takes form submissions from Typeform, Google Forms, Jotform, Tally, or a plain website form, cleans them up, skips duplicates, and adds them to one tidy spreadsheet. The ones that can't go in land in a list that says exactly why, and you can fix them and retry. It's the kind of automation a small business would pay someone to set up.

Honest limits

All state lives in memory, so a page reload clears the inbox. The webhook is local-only and was fed fixtures and curl calls. Nothing in the build records a real form tool posting to it or the connector skill writing to a live Google Sheet. Phones default to US and slash dates are read month-first. Google Fonts were blocked in the build sandbox, so the screenshots show fallback fonts.

How I built it

I told Claude Cowork to one-shot all the Cowork projects and gave it the spec for this one: five sources, a declarative mapping with fuzzy suggestions, normalization, idempotency, a dead letter with retry, CSV and a real .xlsx, a local webhook, and a Cowork skill. It specced, built, tested, screenshotted, and documented it in one pass, about 0.9 hours.

The whole pipeline is a pure reducer in sluice.js. One function turns a raw body into a stage-by-stage trace, and another folds that trace into state. Claude Cowork wrote realistic fixture payloads for every source with planted failures: a bad email, a missing email with Feb 30, a Canadian province, an HTML error page, and two replays. There's a local POST /hook endpoint so I can curl payloads straight into the inbox, and the Cowork skill runs the same rules against a form connector or Gmail with a preview before anything touches a Google Sheet.

The .xlsx is built from scratch: SpreadsheetML parts, a CRC-32 table, and a STORE zip writer. Claude Cowork didn't trust its own writer. It checked the file with an independent zip reader in the tests, then opened it with openpyxl and converted it with LibreOffice.

What I learned

  • Dedupe needs two kinds of key. If the form tool sends a response id, hash source|id. If it doesn't, hash normalized content with lists sorted and case folded, or the same answers in a different order become a second row.
  • A fixed dead-letter row has to keep its original key. Otherwise the next webhook retry of the original bad payload doesn't match and lands in the dead letter again.
  • A minimal .xlsx is only seven or eight small XML parts in a zip.- A CSV formula-injection guard can't blindly prefix cells starting with +, or every phone number like +14155550142 gets mangled.
  • The first screenshot caught real bugs: "Tomás o'brien" and an "E-mail" header shown as "E Mail." Name cleanup now only fixes words typed all lowercase.

What carried forward

Sluice uses the same shape as the June projects: a DOM-free core shared by the page and verify.mjs, plus a zero-dependency serve.mjs (this one grew a webhook inbox). The idempotency and dead-letter thinking is the same problem #52 was solving with payment events, from a different angle. No later project imports Sluice's code.

If I took it further

I would add per-form mappings keyed by form id, let the phone default country come from the config, and persist the pipeline so it survives a reload.

#053 Built with Claude Cowork September 23, 2026 323 automated checks

Want one like this?