Batch audio processing from Google Sheets with n8n

View Markdown

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

  1. A schedule trigger (or Google Sheets trigger) kicks off the workflow on an interval
  2. n8n reads all rows from the sheet where the status column is empty or pending
  3. A SplitInBatches node iterates through rows, sending one Ittybit task per file
  4. Each task POSTs to /tasks with kind: "audio" and a webhook callback URL
  5. A Wait node pauses until Ittybit POSTs back with the completed result
  6. 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:

Rowsource_urlformatstatusoutput_url
2https://example.com/interview-01.wavmp3
3https://example.com/interview-02.wavaac
4https://example.com/field-recording.flacmp3

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 }} (either completed or failed)
  • 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:

OptionValuesDefault
formatmp3, aac, ogg, flac, wavmp3
qualitylow, medium, highhigh

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 failed to 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