# Quickstart

> Build and run your first pipedog pipeline with a custom frame processor.

Source: https://polvallverdu.github.io/pipedog/docs/quickstart/



This quickstart builds the smallest useful pipedog program: a pipeline of two custom frame
processors, run by a `PipelineWorker` inside a `WorkerRunner`. No API keys, no network.

<Callout title="Want a voice bot?">
  The [voice bot guide](https://polvallverdu.github.io/pipedog/docs/voice-bot.md) runs an assistant you talk to in the browser, with OpenAI
  speech-to-text, chat and text-to-speech over SmallWebRTC. [Examples](https://polvallverdu.github.io/pipedog/docs/examples.md) lists every
  runnable example, including the same bot over a WebSocket.
</Callout>

<Steps>
  <Step>
    ### Set up a project [#set-up-a-project]

    Follow [Installation](https://polvallverdu.github.io/pipedog/docs/installation.md) to get `@pipedog/core` and `@pipedog/asyncio`
    into a project. Any of Bun, Node, Deno or Cloudflare Workers works; this page uses Bun.
  </Step>

  <Step>
    ### Write the bot [#write-the-bot]

    Create `quickstart.ts`. It defines two processors: one upper-cases every `TextFrame`, the other
    prints the text frames that reach it. Both pass every other frame through untouched.

    ```ts twoslash title="quickstart.ts"
    import { EndFrame, type Frame, TextFrame } from "@pipedog/core/frames/frames";
    import { Pipeline } from "@pipedog/core/pipeline/pipeline";
    import { PipelineWorker } from "@pipedog/core/pipeline/worker";
    import { type FrameDirection, FrameProcessor } from "@pipedog/core/processors/frame_processor";
    import { WorkerRunner } from "@pipedog/core/workers/runner";

    /** Upper-cases every TextFrame and passes every other frame through. */
    class UpperCaseProcessor extends FrameProcessor {
      override async processFrame(frame: Frame, direction: FrameDirection): Promise<void> {
        await super.processFrame(frame, direction);

        if (frame instanceof TextFrame) {
          await this.pushFrame(new TextFrame({ text: frame.text.toUpperCase() }), direction);
        } else {
          await this.pushFrame(frame, direction);
        }
      }
    }

    /** Prints the text frames that reach it. */
    class PrintProcessor extends FrameProcessor {
      override async processFrame(frame: Frame, direction: FrameDirection): Promise<void> {
        await super.processFrame(frame, direction);

        if (frame instanceof TextFrame) {
          console.log(`${this.name}: ${frame.text}`);
        }
        await this.pushFrame(frame, direction);
      }
    }

    const pipeline = new Pipeline([new UpperCaseProcessor(), new PrintProcessor()]);
    const worker = new PipelineWorker(pipeline, { enableRtvi: false });

    worker.eventHandler("on_pipeline_started", async () => {
      await worker.queueFrames([
        new TextFrame({ text: "hello" }),
        new TextFrame({ text: "from pipedog" }),
        new EndFrame(),
      ]);
    });

    worker.eventHandler("on_pipeline_finished", async (_worker: PipelineWorker, frame: Frame) => {
      console.log(`pipeline finished with ${frame.name}`);
    });

    const runner = new WorkerRunner({ handleSigint: false });
    await runner.addWorkers(worker);
    await runner.run();
    ```

    Hover over the code to see the types: this sample is type-checked against the real
    `@pipedog/core` sources when the docs are built.
  </Step>

  <Step>
    ### Run it [#run-it]

    ```bash
    bun quickstart.ts
    ```

    Among the debug logs you will see:

    ```text
    PrintProcessor#0: HELLO
    PrintProcessor#0: FROM PIPEDOG
    pipeline finished with EndFrame#0
    ```
  </Step>
</Steps>

## What just happened [#what-just-happened]

1. `new Pipeline([...])` linked the processors in order, between a source and a sink.
2. `new PipelineWorker(pipeline, ...)` wrapped the pipeline with lifecycle management. When the
   runner started it, it pushed a `StartFrame` through the pipeline; once that reached the end,
   `on_pipeline_started` fired.
3. The handler queued two `TextFrame`s and an `EndFrame`. The `EndFrame` is a control frame, so
   it is processed in order, after the text frames, and ends the pipeline gracefully once
   everything ahead of it is flushed.
4. `runner.run()` returned once its only worker finished.

`enableRtvi: false` turns off the RTVI processor the worker adds by default, since this bot has no
client. `handleSigint: false` keeps the runner from installing a Ctrl-C handler; leave it at its
default (`true`) in a real bot.

## Next steps [#next-steps]

<Cards>
  <Card title="Examples" href="/docs/examples">
    Runnable voice bots over WebRTC, WebSocket and Durable Objects.
  </Card>

  <Card title="Frames" href="/docs/concepts/frames">
    System, data and control frames, and how interruptions treat them.
  </Card>

  <Card title="Frame processors" href="/docs/concepts/frame-processors">
    `processFrame`, `pushFrame`, directions, setup and event handlers.
  </Card>

  <Card title="Pipelines & workers" href="/docs/concepts/pipelines-and-workers">
    `PipelineParams`, worker events, ending and cancelling.
  </Card>

  <Card title="Interruptions & cancellation" href="/docs/concepts/interruptions-and-cancellation">
    How `@pipedog/asyncio` gives JavaScript asyncio's cancellation.
  </Card>
</Cards>
