# Batch audio processing from Google Sheets with n8n

Process audio files tracked in Google Sheets using n8n batch workflows and Ittybit

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](https://n8n.io) 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.

<CodeGroup labels={["n8n node config", "curl"]}>
```json
{
  "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 }) }}"
      }
    ]
  }
}
```

```bash
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
    }
  }'
```

</CodeGroup>

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:

```json
{
  "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**:

```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.

<CodeGroup labels={["n8n expression", "curl"]}>
```json
{
  "name": "options",
  "value": "={{ JSON.stringify({ format: $json.format || 'mp3', quality: $json.quality || 'high' }) }}"
}
```

```bash
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"
    }
  }'
```

</CodeGroup>

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 `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

- [Slack media bot with n8n](/guides/slack-media-bot-with-n8n) -- event-driven processing from Slack
- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- multi-task processing for uploads
- [n8n HTTP Request node docs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/)
- [n8n Google Sheets node docs](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets/)