Cleans a research dataset by fixed rules and writes down every change, exclusion and unresolved value as it makes it.
Uses
Claude
Google 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.
spreadsheetUrl to blank.Set spreadsheetUrl to blank.rawTab to Raw.Set rawTab to Raw.cleanTab to Clean.Set cleanTab to Clean.logTab to Cleaning log.Set logTab to Cleaning log.reviewTab to Needs review.Set reviewTab to Needs review.runsTab to Runs.Set runsTab to Runs.rulesetVersion to v1.Set rulesetVersion to v1.missingToken to NA.Set missingToken to NA.maxRows to 500.Set maxRows to 500.batchSize to 100.Set batchSize to 100.writeMethodsNote to true.Set writeMethodsNote to true.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:["%.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.runId.Generate a short ID code and save it as runId.stampedAt.Get the current date and time and save it as stampedAt.spreadsheetUrl. Save it as raw.Read the rows of spreadsheetUrl. Save it as raw.rows to raw.rows.Set rows to raw.rows.headers to raw.headers.Set headers to raw.headers.rows and save it as totalRows.Count the items in rows and save it as totalRows.totalRows is 0If totalRows is 0totalRows is greater than maxRowsIf totalRows is greater than maxRowsrows as text and save it as payloadChars.Count the characters in rows as text and save it as payloadChars.payloadChars is greater than 700000If payloadChars is greater than 700000config to a record of missingToken, columnRules, and rowRules.Set config to a record of missingToken, columnRules, and rowRules.cleanRows to an empty list.Set cleanRows to an empty list.logRows to an empty list.Set logRows to an empty list.reviewRows to an empty list.Set reviewRows to an empty list.configErrors to an empty list.Set configErrors to an empty list.fatal to blank.Set fatal to blank.offset to 0.Set offset to 0.offset is less than totalRows:While offset is less than totalRows: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.batch.fatal isn't emptyIf batch.fatal isn't emptyfatal to batch.fatal.Change fatal to batch.fatal.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.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.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.configErrors to batch.config_errors.Change configErrors to batch.config_errors.offset to offset plus batchSize.Change offset to offset plus batchSize.fatal isn't emptyIf fatal isn't emptyfatal.Stop and report a problem: fatal.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.tabName in [cleanTab, logTab, reviewTab]:For each tabName in [cleanTab, logTab, reviewTab]:tabErrorIf a step below fails, save the error as tabErrortabName in spreadsheetUrl.Create the tab tabName in spreadsheetUrl.tabName.Show in the output: Reusing the existing tab: tabName.runsTabErrorIf a step below fails, save the error as runsTabErrorrunsTab in spreadsheetUrl.Create the tab runsTab in spreadsheetUrl.runsTab in spreadsheetUrl.Add a row to runsTab in spreadsheetUrl.Appending to the existing Runs tab..Show in the output: Appending to the existing Runs tab..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].writingTab to blank.Set writingTab to blank.rowsWritten to 0.Set rowsWritten to 0.writeErrorIf a step below fails, save the error as writeErroroutput in outputTabs:For each output in outputTabs:writingTab to output.tab. Change writingTab to output.tab.rowsWritten to 0. Change rowsWritten to 0.output.tab in spreadsheetUrl. Clear the tab output.tab in spreadsheetUrl.output.tab in spreadsheetUrl. Add a row to output.tab in spreadsheetUrl.outputRow in output.rows: For each outputRow in output.rows:output.tab in spreadsheetUrl. Add a row to output.tab in spreadsheetUrl.rowsWritten. Add 1 to rowsWritten.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.markerErrorIf a step below fails, save the error as markerErrorwritingTab in spreadsheetUrl.Add a row to writingTab in spreadsheetUrl.markerError.message.Show in the output: Could not mark the '@writingTab' tab as incomplete: markerError.message.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.methodsNote to blank.Set methodsNote to blank.writeMethodsNote is yesIf writeMethodsNote is yesnoteErrorIf a step below fails, save the error as noteErrorsummary 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: 700note.response_text and save it as methodsNote.Trim note.response_text and save it as methodsNote.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.runsTab in spreadsheetUrl.Add a row to runsTab in spreadsheetUrl.summary.Show in the output: summary.