Batch audio processing from Google Sheets with n8n
Your team tracks recordings in a Google Sheet — one row per file, with columns for the source URL, status, and output link. Every time someone adds a new row, the file just sits there waiting. With n8n and Ittybit, you can wire up a workflow that reads unprocessed rows, fans out audio processing tasks, waits for results, and writes the output URLs back to the sheet. No code to deploy, no servers to babysit.
How it works
- A schedule trigger (or Google Sheets trigger) kicks off the workflow on an interval
- n8n reads all rows from the sheet where the status column is empty or
pending - A SplitInBatches node iterates through rows, sending one Ittybit task per file
- Each task POSTs to
/taskswithkind: "audio"and a webhook callback URL - A Wait node pauses until Ittybit POSTs back with the completed result
- n8n writes the output URL and status back to the corresponding row in the sheet
Prerequisites
- An n8n instance (cloud or self-hosted)
- A Google Sheet with columns:
source_url,status,output_url(and a row number or ID column) - Google Sheets credentials configured in n8n
- An Ittybit API key
Set up the spreadsheet
Your sheet needs at minimum these columns:
| Row | source_url | format | status | output_url |
|---|---|---|---|---|
| 2 | https://example.com/interview-01.wav | mp3 | ||
| 3 | https://example.com/interview-02.wav | aac | ||
| 4 | https://example.com/field-recording.flac | mp3 |
The format column is optional — if empty, the workflow defaults to mp3. The status and output_url columns start blank and get filled in by n8n as tasks complete.
Build the n8n workflow
1. Schedule Trigger node
Add a Schedule Trigger node that runs every 15 minutes (or whatever interval suits your team). Alternatively, use a Google Sheets Trigger node if you want the workflow to fire as soon as a new row appears.
2. Google Sheets node: read unprocessed rows
Add a Google Sheets node with operation Read Rows. Point it at your spreadsheet and sheet name. Use a filter to only return rows where the status column is empty or pending.
The node outputs an array of items, one per row. Each item includes the row number, which you need later to write results back.
3. IF node: check for work
Add an IF node that checks whether any rows came back. Condition: {{ $json.source_url }} is not empty. If no rows match, the workflow ends early without doing anything.
4. SplitInBatches node
Add a SplitInBatches node with batch size 1. This processes one row at a time so each Ittybit task gets its own Wait node cycle. Set Options > Reset to false so the node remembers its position across iterations.
Processing one at a time keeps things simple and avoids overwhelming the API. If you need higher throughput, increase the batch size and add parallel Wait nodes — but for most spreadsheet-scale workloads, sequential processing is fine.
5. HTTP Request node: create the Ittybit task
This is the core node. Add an HTTP Request node that POSTs to the Ittybit Tasks API.
{
"method": "POST",
"url": "https://api.ittybit.com/jobs",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "Bearer {{ $credentials.ittybitApiKey }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{ "name": "input", "value": "={{ $json.source_url }}" },
{ "name": "kind", "value": "audio" },
{
"name": "options",
"value": "={{ JSON.stringify({ format: $json.format || 'mp3', quality: 'high' }) }}"
},
{
"name": "webhook_url",
"value": "={{ $node['Wait for Ittybit'].webhookUrl }}"
},
{
"name": "metadata",
"value": "={{ JSON.stringify({ row_number: $json.row_number }) }}"
}
]
}
}curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://example.com/interview-01.wav",
"kind": "audio",
"options": {
"format": "mp3",
"quality": "high"
},
"webhook_url": "https://your-n8n.app/webhook-waiting/abc123",
"metadata": {
"row_number": 2
}
}' The webhook_url points to the Wait node so Ittybit can resume the workflow when processing finishes. The metadata.row_number carries through unchanged so you know which spreadsheet row to update.
6. Wait node: pause for Ittybit callback
Add a Wait node set to Resume on webhook call. This pauses the workflow until Ittybit POSTs back with the result. The Wait node generates the unique webhook URL you referenced in step 5.
When Ittybit finishes processing, it POSTs to the webhook URL:
{
"id": "task_abc123",
"status": "completed",
"kind": "audio",
"output": {
"url": "https://cdn.ittybit.com/o/output/interview-01.mp3"
},
"metadata": {
"row_number": 2
}
}
7. Google Sheets node: write results back
Add a Google Sheets node with operation Update Row. Use the row_number from the webhook payload metadata to target the correct row. Set:
- status:
{{ $json.status }}(eithercompletedorfailed) - output_url:
{{ $json.output.url }}
For failed tasks, the output URL will be empty, but the status column tells your team which files need attention.
8. Loop back to SplitInBatches
Connect the Google Sheets update node back to the SplitInBatches node. This closes the loop — n8n processes the next row, creates the next task, waits for the next callback, writes the next result, and repeats until all rows are done.
Full workflow JSON
Import this directly into n8n via Settings > Import from JSON:
{
"name": "Batch Audio from Google Sheets",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "minutes", "minutesInterval": 15 }] } },
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [240, 300]
},
{
"parameters": {
"operation": "read",
"documentId": { "value": "YOUR_SPREADSHEET_ID" },
"sheetName": { "value": "Sheet1" },
"filters": {
"conditions": [{ "condition": "isEmpty", "field": "status" }]
}
},
"name": "Read Unprocessed Rows",
"type": "n8n-nodes-base.googleSheets",
"position": [460, 300]
},
{
"parameters": {
"conditions": {
"string": [{ "value1": "={{ $json.source_url }}", "operation": "isNotEmpty" }]
}
},
"name": "Has Rows?",
"type": "n8n-nodes-base.if",
"position": [680, 300]
},
{
"parameters": { "batchSize": 1, "options": {} },
"name": "SplitInBatches",
"type": "n8n-nodes-base.splitInBatches",
"position": [900, 260]
},
{
"parameters": {
"method": "POST",
"url": "https://api.ittybit.com/jobs",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "Bearer {{ $credentials.ittybitApiKey }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={ \"input\": \"{{ $json.source_url }}\", \"kind\": \"audio\", \"options\": { \"format\": \"{{ $json.format || 'mp3' }}\", \"quality\": \"high\" }, \"webhook_url\": \"{{ $node['Wait for Ittybit'].webhookUrl }}\", \"metadata\": { \"row_number\": {{ $json.row_number }} } }"
},
"name": "Create Ittybit Task",
"type": "n8n-nodes-base.httpRequest",
"position": [1120, 260]
},
{
"parameters": { "resume": "webhook" },
"name": "Wait for Ittybit",
"type": "n8n-nodes-base.wait",
"position": [1340, 260]
},
{
"parameters": {
"operation": "update",
"documentId": { "value": "YOUR_SPREADSHEET_ID" },
"sheetName": { "value": "Sheet1" },
"columns": {
"mappingMode": "defineBelow",
"value": {
"status": "={{ $json.status }}",
"output_url": "={{ $json.output.url }}"
}
},
"options": {
"cellFormat": "USER_ENTERED",
"rowToUpdate": "={{ $json.metadata.row_number }}"
}
},
"name": "Update Sheet Row",
"type": "n8n-nodes-base.googleSheets",
"position": [1560, 260]
}
],
"connections": {
"Schedule Trigger": {
"main": [[{ "node": "Read Unprocessed Rows", "type": "main", "index": 0 }]]
},
"Read Unprocessed Rows": { "main": [[{ "node": "Has Rows?", "type": "main", "index": 0 }]] },
"Has Rows?": { "main": [[{ "node": "SplitInBatches", "type": "main", "index": 0 }], []] },
"SplitInBatches": { "main": [[{ "node": "Create Ittybit Task", "type": "main", "index": 0 }]] },
"Create Ittybit Task": {
"main": [[{ "node": "Wait for Ittybit", "type": "main", "index": 0 }]]
},
"Wait for Ittybit": { "main": [[{ "node": "Update Sheet Row", "type": "main", "index": 0 }]] },
"Update Sheet Row": { "main": [[{ "node": "SplitInBatches", "type": "main", "index": 0 }]] }
}
}
Customise processing options
The options object in the task body controls how Ittybit processes each file. Adjust these per row by reading additional columns from the spreadsheet.
{
"name": "options",
"value": "={{ JSON.stringify({ format: $json.format || 'mp3', quality: $json.quality || 'high' }) }}"
}curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": "https://example.com/interview-01.wav",
"kind": "audio",
"options": {
"format": "aac",
"quality": "medium"
}
}' Common audio options:
| Option | Values | Default |
|---|---|---|
format | mp3, aac, ogg, flac, wav | mp3 |
quality | low, medium, high | high |
Add format and quality columns to your spreadsheet to control these per file. Leave them empty to use the defaults.
Error handling
Add an IF node after the Wait node to branch on {{ $json.status }}:
- completed — continue to the Google Sheets update node
- failed — route to a separate Google Sheets update node that writes
failedto the status column
For rows that fail, your team can fix the source URL in the sheet, clear the status, and let the next scheduled run pick them up again automatically.
If the HTTP Request node itself fails (network error, invalid API key), n8n’s built-in retry settings handle it. Set retries to 2 with a backoff of 5000ms on the HTTP Request node under Settings > Error Handling.
See also
- Slack media bot with n8n — event-driven processing from Slack
- Build a user upload pipeline — multi-task processing for uploads
- n8n HTTP Request node docs
- n8n Google Sheets node docs