Cleans a research dataset by fixed rules and writes down every change, exclusion and unresolved value as it makes it.

UsesClaudeGoogle Sheets

Research data cleaning with a decision log

Once a day, the raw tab of your research spreadsheet is cleaned by fixed rules into an analysis-ready dataset, with a written record of how it got that way. Values are trimmed and normalised, blanks collapse to one token, numbers, dates and identifiers are parsed, and free text is mapped to your controlled vocabulary. Rows are excluded for duplicates, withheld consent, failed attention checks, speeding, straightlining, missing required values or timestamps outside your collection window, each logged with its reason.

You end up with a 'Clean' tab, a 'Cleaning log' with one line per decision, a 'Needs review' tab, and a 'Runs' tab holding your participant-flow history with a methods paragraph written by AI from the counts alone. Your raw tab is never written to. A rule that cannot decide does not decide: an ambiguous date or an unknown answer is left as it arrived and sent for a person to resolve. If a write fails partway, the run stops and marks that tab incomplete.

ScheduleRun every day.Run every day.
ValuesSet spreadsheetUrl to blank.Set spreadsheetUrl to blank.
ValuesSet rawTab to Raw.Set rawTab to Raw.
ValuesSet cleanTab to Clean.Set cleanTab to Clean.
ValuesSet logTab to Cleaning log.Set logTab to Cleaning log.
ValuesSet reviewTab to Needs review.Set reviewTab to Needs review.
ValuesSet runsTab to Runs.Set runsTab to Runs.
ValuesSet rulesetVersion to v1.Set rulesetVersion to v1.
ValuesSet missingToken to NA.Set missingToken to NA.
ValuesSet maxRows to 500.Set maxRows to 500.
ValuesSet batchSize to 100.Set batchSize to 100.
ValuesSet writeMethodsNote to true.Set writeMethodsNote to true.
ValuesSet columnRules to {participant_id:{type:id,pattern:^P[0-9]{4}$,required:true},submitted_at:{type:datetime,formats:["%.Set columnRules to {participant_id:{type:id,pattern:^P[0-9]{4}$,required:true},submitted_at:{type:datetime,formats:["%.
ValuesSet rowRules to {consentColumn:consent,duplicateKey:[participant_id],duplicateKeep:first,deduplicateWholeRows:true,"atten.Set rowRules to {consentColumn:consent,duplicateKey:[participant_id],duplicateKeep:first,deduplicateWholeRows:true,"atten.
RandomGenerate a short ID code and save it as runId.Generate a short ID code and save it as runId.
Dates & TimesGet the current date and time and save it as stampedAt.Get the current date and time and save it as stampedAt.
Google SheetsGoogle SheetsRead the rows of spreadsheetUrl. Save it as raw.Read the rows of spreadsheetUrl. Save it as raw.
ValuesSet rows to raw.rows.Set rows to raw.rows.
ValuesSet headers to raw.headers.Set headers to raw.headers.
ListsCount the items in rows and save it as totalRows.Count the items in rows and save it as totalRows.
Decisions & LoopsIf totalRows is 0If totalRows is 0
Yes
Decisions & LoopsStop and report a problem: The raw tab has no data rows, so there is nothing to clean. Check that 'rawTab' names the right tab and that its first row is the column headers.Stop and report a problem: The raw tab has no data rows, so there is nothing to clean. Check that 'rawTab' names the right tab and that its first row is the column headers.
No
No steps
Decisions & LoopsIf totalRows is greater than maxRowsIf totalRows is greater than maxRows
Yes
Decisions & LoopsStop and report a problem: The raw tab has more rows than 'maxRows' allows. Raise 'maxRows' if the run has time for it, or clean the data in slices — this workflow will not write a partial dataset that would look complete.Stop and report a problem: The raw tab has more rows than 'maxRows' allows. Raise 'maxRows' if the run has time for it, or clean the data in slices — this workflow will not write a partial dataset that would look complete.
No
No steps
TextCount the characters in rows as text and save it as payloadChars.Count the characters in rows as text and save it as payloadChars.
Decisions & LoopsIf payloadChars is greater than 700000If payloadChars is greater than 700000
Yes
Decisions & LoopsStop and report a problem: The raw rows are too large to hand to the cleaning step in one piece (its input is capped at about 1 MB). Lower 'maxRows', or split the raw tab, and run again. Nothing was written.Stop and report a problem: The raw rows are too large to hand to the cleaning step in one piece (its input is capped at about 1 MB). Lower 'maxRows', or split the raw tab, and run again. Nothing was written.
No
No steps
ValuesSet config to a record of missingToken, columnRules, and rowRules.Set config to a record of missingToken, columnRules, and rowRules.
ValuesSet cleanRows to an empty list.Set cleanRows to an empty list.
ValuesSet logRows to an empty list.Set logRows to an empty list.
ValuesSet reviewRows to an empty list.Set reviewRows to an empty list.
ValuesSet configErrors to an empty list.Set configErrors to an empty list.
ValuesSet fatal to blank.Set fatal to blank.
ValuesSet offset to 0.Set offset to 0.
Decisions & LoopsWhile offset is less than totalRows:While offset is less than totalRows:
CodeRun Python code on rows, headers, config, window, run_id, ruleset_version, and stamped_at and save it as batch.Run Python code on rows, headers, config, window, run_id, ruleset_version, and stamped_at and save it as batch.
Decisions & LoopsIf batch.fatal isn't emptyIf batch.fatal isn't empty
Yes
ValuesChange fatal to batch.fatal.Change fatal to batch.fatal.
Decisions & LoopsStop repeating.Stop repeating.
No
No steps
ListsCombine cleanRows and batch.clean into one list and save it as cleanRows.Combine cleanRows and batch.clean into one list and save it as cleanRows.
ListsCombine logRows and batch.log into one list and save it as logRows.Combine logRows and batch.log into one list and save it as logRows.
ListsCombine reviewRows and batch.review into one list and save it as reviewRows.Combine reviewRows and batch.review into one list and save it as reviewRows.
ValuesChange configErrors to batch.config_errors.Change configErrors to batch.config_errors.
MathChange offset to offset plus batchSize.Change offset to offset plus batchSize.
Decisions & LoopsIf fatal isn't emptyIf fatal isn't empty
Yes
Decisions & LoopsStop and report a problem: fatal.Stop and report a problem: fatal.
No
No steps
CodeRun Python code on clean_count, log, review, total, and config_errors and save it as summary.Run Python code on clean_count, log, review, total, and config_errors and save it as summary.
Decisions & LoopsFor each tabName in [cleanTab, logTab, reviewTab]:For each tabName in [cleanTab, logTab, reviewTab]:
Decisions & LoopsIf a step below fails, save the error as tabErrorIf a step below fails, save the error as tabError
Try
Google SheetsGoogle SheetsCreate the tab tabName in spreadsheetUrl.Create the tab tabName in spreadsheetUrl.
On error
ValuesShow in the output: Reusing the existing tab: tabName.Show in the output: Reusing the existing tab: tabName.
Decisions & LoopsIf a step below fails, save the error as runsTabErrorIf a step below fails, save the error as runsTabError
Try
Google SheetsGoogle SheetsCreate the tab runsTab in spreadsheetUrl.Create the tab runsTab in spreadsheetUrl.
Google SheetsGoogle SheetsAdd a row to runsTab in spreadsheetUrl.Add a row to runsTab in spreadsheetUrl.
On error
ValuesShow in the output: Appending to the existing Runs tab..Show in the output: Appending to the existing Runs tab..
ValuesSet outputTabs to [a record of tab, header, and rows, a record of tab, header, and rows, a record of tab, header, and rows].Set outputTabs to [a record of tab, header, and rows, a record of tab, header, and rows, a record of tab, header, and rows].
ValuesSet writingTab to blank.Set writingTab to blank.
ValuesSet rowsWritten to 0.Set rowsWritten to 0.
Decisions & LoopsIf a step below fails, save the error as writeErrorIf a step below fails, save the error as writeError
Try
Decisions & LoopsFor each output in outputTabs:For each output in outputTabs:
Values Change writingTab to output.tab. Change writingTab to output.tab.
Values Change rowsWritten to 0. Change rowsWritten to 0.
Google SheetsGoogle Sheets Clear the tab output.tab in spreadsheetUrl. Clear the tab output.tab in spreadsheetUrl.
Google SheetsGoogle Sheets Add a row to output.tab in spreadsheetUrl. Add a row to output.tab in spreadsheetUrl.
Decisions & Loops For each outputRow in output.rows: For each outputRow in output.rows:
Google SheetsGoogle Sheets Add a row to output.tab in spreadsheetUrl. Add a row to output.tab in spreadsheetUrl.
Values Add 1 to rowsWritten. Add 1 to rowsWritten.
On error
ValuesSet incompleteNote to INCOMPLETE: run runId cleared this tab and then failed after writing rowsWritten as text data rows (writeError.message). Run the workflow again to rebuild it from the raw tab.Set incompleteNote to INCOMPLETE: run runId cleared this tab and then failed after writing rowsWritten as text data rows (writeError.message). Run the workflow again to rebuild it from the raw tab.
Decisions & LoopsIf a step below fails, save the error as markerErrorIf a step below fails, save the error as markerError
Try
Google SheetsGoogle SheetsAdd a row to writingTab in spreadsheetUrl.Add a row to writingTab in spreadsheetUrl.
On error
ValuesShow in the output: Could not mark the '@writingTab' tab as incomplete: markerError.message.Show in the output: Could not mark the '@writingTab' tab as incomplete: markerError.message.
Decisions & LoopsStop and report a problem: Writing the '@writingTab' tab failed after rowsWritten as text data rows: writeError.message. The Clean, Cleaning log and Needs review tabs are rebuilt in that order on every run, so tabs rebuilt before this one hold this run's output, tabs after it still hold the previous run's, and '@writingTab' is marked INCOMPLETE in its last row. Nothing was added to the Runs tab and the raw tab was not touched. Fix the cause and run again: a full run rebuilds all three tabs.Stop and report a problem: Writing the '@writingTab' tab failed after rowsWritten as text data rows: writeError.message. The Clean, Cleaning log and Needs review tabs are rebuilt in that order on every run, so tabs rebuilt before this one hold this run's output, tabs after it still hold the previous run's, and '@writingTab' is marked INCOMPLETE in its last row. Nothing was added to the Runs tab and the raw tab was not touched. Fix the cause and run again: a full run rebuilds all three tabs.
ValuesSet methodsNote to blank.Set methodsNote to blank.
Decisions & LoopsIf writeMethodsNote is yesIf writeMethodsNote is yes
Yes
Decisions & LoopsIf a step below fails, save the error as noteErrorIf a step below fails, save the error as noteError
Try
ClaudeClaudeAsk Claude: 'You are writing the data-cleaning paragraph of a research methods section.Ask Claude: 'You are writing the data-cleaning paragraph of a research methods section. Here are the counts from one automated cleaning run. They are the only facts you have: rule names and totals, no participant data. summary as text Ruleset version: rulesetVersion Write one paragraph, 80-120 words, past tense, third person, no bullet points and no heading. State how many records were read, how many were excluded and under which rules, how many values were changed and how many were left for manual review. Name the ruleset version. Describe only what these numbers say: do not judge the data quality, do not speculate about causes, do not recommend anything, and do not mention any figure that is not above. If reconciles is false, say plainly that the participant counts do not reconcile and that the log needs checking.'. Save it as note. Maximum response length: 700
TextTrim note.response_text and save it as methodsNote.Trim note.response_text and save it as methodsNote.
On error
ValuesChange methodsNote to The methods note could not be written; the cleaned data and the log are unaffected. Reason: noteError.message.Change methodsNote to The methods note could not be written; the cleaned data and the log are unaffected. Reason: noteError.message.
No
No steps
Google SheetsGoogle SheetsAdd a row to runsTab in spreadsheetUrl.Add a row to runsTab in spreadsheetUrl.
ValuesShow in the output: summary.Show in the output: summary.