# Event-driven video processing with Inngest and Ittybit

Use Inngest step functions to orchestrate video transcoding and thumbnail generation

Inngest step functions give you durable, retryable workflows without managing queues. Pair them with Ittybit and you get an event-driven pipeline that transcodes video, extracts thumbnails, and fans out to multiple formats -- all triggered by a single upload event.

## Send the event

When a video is uploaded, fire a `video/uploaded` event from your API route or webhook handler. Inngest picks it up and runs the function.

```typescript

// In your upload handler
await inngest.send({
  name: 'video/uploaded',
  data: {
    videoId: 'vid_abc123',
    sourceUrl: 'https://your-bucket.s3.amazonaws.com/uploads/raw.mov',
  },
});
```

## Define the function

Each `step.run` is independently retried on failure. If the transcode step succeeds but the thumbnail step fails, Inngest retries only the thumbnail -- it won't re-transcode.

```typescript

const ITTYBIT_API = 'https://api.ittybit.com';

async function createTask(body: Record<string, unknown>) {
  const res = await fetch(`${ITTYBIT_API}/tasks`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`Ittybit API error: ${res.status}`);
  return res.json();
}

async function pollTask(taskId: string) {
  while (true) {
    const res = await fetch(`${ITTYBIT_API}/tasks/${taskId}`, {
      headers: { Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}` },
    });
    const task = await res.json();
    if (task.status === 'completed') return task;
    if (task.status === 'failed') throw new Error(`Task ${taskId} failed`);
    await new Promise((r) => setTimeout(r, 5000));
  }
}

export const processVideo = inngest.createFunction(
  { id: 'process-video', retries: 3 },
  { event: 'video/uploaded' },
  async ({ event, step }) => {
    const { videoId, sourceUrl } = event.data;

    // Step 1: Transcode to MP4
    const transcode = await step.run('transcode', async () => {
      const task = await createTask({
        input: sourceUrl,
        kind: 'video',
        options: { width: 1920, format: 'mp4', quality: 'high' },
        metadata: { videoId },
      });
      return pollTask(task.id);
    });

    // Step 2: Extract thumbnail from the transcoded video
    const thumbnail = await step.run('thumbnail', async () => {
      const task = await createTask({
        input: transcode.output.url,
        kind: 'image',
        options: { start: 2, width: 640, format: 'webp' },
        metadata: { videoId },
      });
      return pollTask(task.id);
    });

    // Step 3: Update DB and notify
    await step.run('notify', async () => {
      await db.video.update({
        where: { id: videoId },
        data: {
          status: 'ready',
          mp4Url: transcode.output.url,
          thumbnailUrl: thumbnail.output.url,
        },
      });

      await sendNotification(videoId, 'Video processing complete');
    });

    return { videoId, status: 'ready' };
  },
);
```

## Fan out to multiple formats

For multiple output formats, use `step.run` inside a loop. Each format gets its own retryable step, and Inngest runs them concurrently.

```typescript
export const processMultipleFormats = inngest.createFunction(
  { id: 'process-multi-format', retries: 3 },
  { event: 'video/uploaded' },
  async ({ event, step }) => {
    const { videoId, sourceUrl } = event.data;

    const formats = [
      { width: 1920, format: 'mp4', quality: 'high', label: '1080p' },
      { width: 1280, format: 'mp4', quality: 'medium', label: '720p' },
      { width: 640, format: 'mp4', quality: 'medium', label: '360p' },
    ];

    const results = await Promise.all(
      formats.map((fmt) =>
        step.run(`transcode-${fmt.label}`, async () => {
          const task = await createTask({
            input: sourceUrl,
            kind: 'video',
            options: { width: fmt.width, format: fmt.format, quality: fmt.quality },
            metadata: { videoId, label: fmt.label },
          });
          return pollTask(task.id);
        }),
      ),
    );

    await step.run('save-results', async () => {
      await db.video.update({
        where: { id: videoId },
        data: {
          status: 'ready',
          variants: results.map((r, i) => ({
            label: formats[i].label,
            url: r.output.url,
          })),
        },
      });
    });

    return { videoId, variants: results.length };
  },
);
```

## Serve the functions

Register your functions with the Inngest serve handler. This works with Next.js, Express, Hono, or any supported framework.

<CodeGroup labels={["Next.js", "Express"]}>
```typescript
// app/api/inngest/route.ts

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [processVideo, processMultipleFormats],
});
```

```typescript
// server.ts

const app = express();

app.use(
  '/api/inngest',
  serve({ client: inngest, functions: [processVideo, processMultipleFormats] }),
);

app.listen(3000);
```

</CodeGroup>

## See also

- [Build a user upload pipeline](/guides/build-a-user-upload-pipeline) -- multi-task processing for uploads
- [Process uploads in Next.js](/guides/process-uploads-in-nextjs) -- Server Actions and webhook callbacks
- [HLS streaming](/guides/create-hls-streams) -- adaptive bitrate output