# Slack media bot with n8n and Ittybit

Build a Slack bot that processes video, audio, and images on demand using n8n and Ittybit

Someone drops a media URL in Slack with `/process`, n8n picks it up, sends it to Ittybit for transcoding, waits for the result, and replies in-thread with the output URL. No code to deploy, no servers to manage.

## How it works

1. A Slack slash command (`/process`) sends the media URL and type to n8n
2. n8n routes the request by media type (video, audio, or image)
3. An HTTP Request node POSTs to Ittybit `/tasks` with a webhook callback URL
4. n8n pauses the workflow and waits for the Ittybit webhook
5. When processing completes, n8n resumes and replies in the original Slack thread

## Prerequisites

- An [n8n](https://n8n.io) instance (cloud or self-hosted)
- A Slack workspace with permission to create a slash command
- An Ittybit API key

## Create the Slack slash command

In the [Slack API dashboard](https://api.slack.com/apps), create a new app and add a slash command:

- **Command:** `/process`
- **Request URL:** Your n8n webhook trigger URL (created in the next step)
- **Description:** `Process a media file with Ittybit`
- **Usage Hint:** `[video|audio|image] [url]`

Install the app to your workspace and note the **Bot User OAuth Token** for the Slack reply node.

## Build the n8n workflow

### 1. Webhook Trigger node

Create a new workflow. Add a **Webhook** node as the trigger. Set the HTTP method to `POST`. Copy the webhook URL and paste it into the Slack slash command Request URL.

The incoming payload from Slack will include:

```json
{
  "text": "video https://example.com/uploads/clip.mov",
  "channel_id": "C01ABC123",
  "user_id": "U01XYZ789",
  "response_url": "https://hooks.slack.com/commands/..."
}
```

### 2. Function node: parse the command

Add a **Code** node to split the slash command text into media type and URL:

```javascript
const text = $input.first().json.text || '';
const parts = text.trim().split(/\s+/);

const kind = ['video', 'audio', 'image'].includes(parts[0]) ? parts[0] : 'video';
const url = parts.find((p) => p.startsWith('http')) || '';

return [
  {
    json: {
      kind,
      url,
      channel_id: $input.first().json.channel_id,
      user_id: $input.first().json.user_id,
    },
  },
];
```

### 3. IF node: validate input

Add an **IF** node to check that a URL was provided. Condition: `{{ $json.url }}` is not empty. Route the "false" branch to a **Respond to Webhook** node that returns an error message to Slack.

### 4. HTTP Request node: create the Ittybit task

This is the core of the workflow. Add an **HTTP Request** node with this config:

<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.url }}" },
      { "name": "kind", "value": "={{ $json.kind }}" },
      {
        "name": "webhook_url",
        "value": "={{ $node['Wait'].webhookUrl }}"
      },
      {
        "name": "metadata",
        "value": "={{ JSON.stringify({ channel_id: $json.channel_id, user_id: $json.user_id }) }}"
      }
    ]
  }
}
```

```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/uploads/clip.mov",
    "kind": "video",
    "webhook_url": "https://your-n8n.app/webhook-waiting/abc123",
    "metadata": {
      "channel_id": "C01ABC123",
      "user_id": "U01XYZ789"
    }
  }'
```

</CodeGroup>

The `webhook_url` points to the n8n Wait node so Ittybit can resume the workflow on completion. The `metadata` field passes through unchanged, carrying the Slack context needed for the reply.

### 5. Respond to Webhook node

Immediately after dispatching the task, send an acknowledgement back to Slack so the user knows the job is in progress. Add a **Respond to Webhook** node:

```json
{
  "respondWith": "text",
  "responseBody": "Processing your {{ $json.kind }} file. I'll reply here when it's done."
}
```

### 6. Wait node: pause for webhook callback

Add a **Wait** node set to **Resume on webhook call**. This pauses the workflow until Ittybit POSTs back. The Wait node generates a unique webhook URL that you passed as `webhook_url` in step 4.

When Ittybit completes the task, it POSTs to this URL with:

```json
{
  "id": "task_abc123",
  "status": "completed",
  "kind": "video",
  "output": {
    "url": "https://cdn.ittybit.com/o/output/video.mp4"
  },
  "metadata": {
    "channel_id": "C01ABC123",
    "user_id": "U01XYZ789"
  }
}
```

### 7. IF node: check task status

Add an **IF** node to branch on `{{ $json.status }}`:

- **completed** -- continue to the Slack reply node
- **failed** -- send an error message instead

### 8. Slack node: reply in thread

Add a **Slack** node (using your Bot User OAuth Token):

- **Operation:** Send Message
- **Channel:** `{{ $json.metadata.channel_id }}`
- **Text:** `<@{{ $json.metadata.user_id }}> Done! Here's your processed {{ $json.kind }}: {{ $json.output.url }}`

For the failure branch, send:

- **Text:** `<@{{ $json.metadata.user_id }}> Processing failed for task {{ $json.id }}. Check the Ittybit dashboard for details.`

## Full workflow JSON

Import this directly into n8n via **Settings > Import from JSON**:

```json
{
  "name": "Slack Media Bot",
  "nodes": [
    {
      "parameters": { "httpMethod": "POST", "path": "slack-media-bot" },
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "position": [240, 300]
    },
    {
      "parameters": {
        "jsCode": "const text = $input.first().json.text || '';\nconst parts = text.trim().split(/\\s+/);\nconst kind = ['video', 'audio', 'image'].includes(parts[0]) ? parts[0] : 'video';\nconst url = parts.find(p => p.startsWith('http')) || '';\nreturn [{ json: { kind, url, channel_id: $input.first().json.channel_id, user_id: $input.first().json.user_id } }];"
      },
      "name": "Parse Command",
      "type": "n8n-nodes-base.code",
      "position": [460, 300]
    },
    {
      "parameters": {
        "conditions": { "string": [{ "value1": "={{ $json.url }}", "operation": "isNotEmpty" }] }
      },
      "name": "Has URL?",
      "type": "n8n-nodes-base.if",
      "position": [680, 300]
    },
    {
      "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.url }}\", \"kind\": \"{{ $json.kind }}\", \"webhook_url\": \"{{ $node['Wait for Ittybit'].webhookUrl }}\", \"metadata\": { \"channel_id\": \"{{ $json.channel_id }}\", \"user_id\": \"{{ $json.user_id }}\" } }"
      },
      "name": "Create Ittybit Task",
      "type": "n8n-nodes-base.httpRequest",
      "position": [900, 260]
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "=Processing your {{ $json.kind }} file. I'll reply here when it's done."
      },
      "name": "Ack to Slack",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [1120, 260]
    },
    {
      "parameters": { "resume": "webhook" },
      "name": "Wait for Ittybit",
      "type": "n8n-nodes-base.wait",
      "position": [1340, 260]
    },
    {
      "parameters": {
        "conditions": { "string": [{ "value1": "={{ $json.status }}", "value2": "completed" }] }
      },
      "name": "Task Completed?",
      "type": "n8n-nodes-base.if",
      "position": [1560, 260]
    },
    {
      "parameters": {
        "channel": "={{ $json.metadata.channel_id }}",
        "text": "=<@{{ $json.metadata.user_id }}> Done! Here's your processed {{ $json.kind }}: {{ $json.output.url }}"
      },
      "name": "Slack Reply (Success)",
      "type": "n8n-nodes-base.slack",
      "position": [1780, 220]
    },
    {
      "parameters": {
        "channel": "={{ $json.metadata.channel_id }}",
        "text": "=<@{{ $json.metadata.user_id }}> Processing failed for task {{ $json.id }}."
      },
      "name": "Slack Reply (Failure)",
      "type": "n8n-nodes-base.slack",
      "position": [1780, 360]
    }
  ],
  "connections": {
    "Webhook Trigger": { "main": [[{ "node": "Parse Command", "type": "main", "index": 0 }]] },
    "Parse Command": { "main": [[{ "node": "Has URL?", "type": "main", "index": 0 }]] },
    "Has URL?": { "main": [[{ "node": "Create Ittybit Task", "type": "main", "index": 0 }], []] },
    "Create Ittybit Task": { "main": [[{ "node": "Ack to Slack", "type": "main", "index": 0 }]] },
    "Ack to Slack": { "main": [[{ "node": "Wait for Ittybit", "type": "main", "index": 0 }]] },
    "Wait for Ittybit": { "main": [[{ "node": "Task Completed?", "type": "main", "index": 0 }]] },
    "Task Completed?": {
      "main": [
        [{ "node": "Slack Reply (Success)", "type": "main", "index": 0 }],
        [{ "node": "Slack Reply (Failure)", "type": "main", "index": 0 }]
      ]
    }
  }
}
```

## Add processing options

Extend the Code node to support extra flags in the slash command. For example, `/process video https://example.com/clip.mov 720p webm`:

```javascript
const text = $input.first().json.text || '';
const parts = text.trim().split(/\s+/);

const kind = ['video', 'audio', 'image'].includes(parts[0]) ? parts[0] : 'video';
const url = parts.find((p) => p.startsWith('http')) || '';
const flags = parts.filter((p) => !p.startsWith('http') && p !== kind);

const options = {};
for (const flag of flags) {
  if (/^\d+p$/.test(flag)) options.height = parseInt(flag);
  if (['mp4', 'webm', 'mov', 'mp3', 'aac', 'webp', 'png', 'jpg'].includes(flag))
    options.format = flag;
  if (['low', 'medium', 'high'].includes(flag)) options.quality = flag;
}

return [
  {
    json: {
      kind,
      url,
      options,
      channel_id: $input.first().json.channel_id,
      user_id: $input.first().json.user_id,
    },
  },
];
```

Then pass `options` through to the Ittybit task body.

## See also

- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- multi-task processing for uploads
- [Media processing pipeline on Railway](/guides/media-processing-pipeline-on-railway) -- webhook-driven backend
- [n8n HTTP Request node docs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/)