---
title: "Stream content into the editor"
description: "Low-level API for streaming content directly into the editor content. Learn more in our docs."
canonical_url: "https://tiptap.dev/docs/content-ai/capabilities/generation/text-generation/stream"
---

# Stream content into the editor

Low-level API for streaming content directly into the editor content. Learn more in our docs.

- **1. Activate trial or subscribe**

  Start a [free trial](https://cloud.tiptap.dev/v2?trial=true) or [subscribe to the Start plan](https://cloud.tiptap.dev/v2/billing) in your account.
- **2. Integrate an AI provider**

  Configure OpenAI in your [AI settings](https://cloud.tiptap.dev/v2/cloud/ai) or review the
  [Custom LLM guides](https://tiptap.dev/docs/content-ai/capabilities/generation/custom-llms.md).
- **3. Install from private registry**

  To install the frontend extensions, authenticate to Tiptap’s private npm registry by following
  the [setup guide](https://tiptap.dev/docs/guides/pro-extensions.md).

The `streamContent` command is a low-level API to stream content into the editor. It supports both appending content and replacing a specified range of content. This command is useful when you need to stream something like an LLM model response into the editor.

> **Advanced Integration:**
>
> This command is useful for advanced integrations where you need to stream content into the editor
> from a URL or a response body.

### Parameters

**range**: Either a single position to insert from or an object specifying the range to replace, with `from` and `to` properties.&#x20;
**callback**: An asynchronous function that receives a write function to stream content into the editor.

### `callback` Arguments

**getWritableStream**: A function that returns a writable stream object that can be used to write chunks of data into the editor.&#x20;
**write**: A function that takes an object with the following properties:

- `partial`: The content to insert into the editor.
- `transform`: An optional function that takes an object with the following properties:
  - `buffer`: The accumulated content of the stream.
  - `partial`: The current partial content.
  - `editor`: The editor instance.
  - `defaultTransform`: The default transform function. This function takes the accumulated content and inserts it into the editor.
- `appendToChain`: An optional function which can be used to append commands to the chain.

## Example Usage

### Using the `write` API

This example shows the flexibility of the `streamContent` command by fetching a large data stream from a URL and streaming it chunk-by-chunk into the editor.

```ts
editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('Failed to get reader from response body.')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })
    write({ partial: chunk })
  }
})
```

### Using the `getWritableStream` API

This example demonstrates an alternative way to stream content using a `WritableStream` object which can be used to write chunks of data into the editor.

```ts
editor.commands.streamContent({ from: 0, to: 10 }, async ({ getWritableStream }) => {
  const response = await fetch('https://example.com/stream')
  // This will pipe the response body content directly into the editor
  await response.body?.pipeTo(getWritableStream())
})
```

### Using transformations

You can also use the `transform` function to modify the content before streaming it into the editor. This example demonstrates how to transform the content before streaming it into the editor.

```ts
editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('Failed to get reader from response body.')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })

    write({
      partial: transformedChunk,
      transform: ({ buffer, partial, editor, defaultTransform }) => {
        // This will use the default transform function to take the whole buffer and insert it into the editor as uppercase
        return defaultTransform(buffer.toUpperCase())
      },
    })
  }
})
```

**Use case:** Parsing markdown content from a URL and streaming it into the editor.

```ts
import { marked } from 'marked'

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  const response = await fetch('https://example.com/stream')
  const reader = response.body?.getReader()
  const decoder = new TextDecoder('utf-8')

  if (!reader) {
    throw new Error('Failed to get reader from response body.')
  }

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })

    write({
      partial: chunk,
      transform: ({ buffer, partial, editor, defaultTransform }) => {
        // This will parse the markdown content into an HTML string and insert it into the editor
        return defaultTransform(marked.parse(buffer))
      },
    })
  }
})
```

### Using `appendToChain` option

The `appendToChain` function allows you to append commands to the chain before it is executed. This example demonstrates how to append a command to the chain before it is executed.

```ts
import { selectionToInsertionEnd } from '@tiptap/core'

editor.commands.streamContent({ from: 0, to: 10 }, async ({ write }) => {
  write({
    partial: token,
    appendToChain: (chain) =>
      chain
        // Move the selection to the end of the inserted content
        .command(({ tr }) => {
          selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
          return true
        })
        // Scroll the editor to the end of the inserted content
        .scrollIntoView(),
  })
})
```

### Using `respondInline` option of `streamContent`

By default `respondInline` is `true`. When inserting block content into the editor, sometimes you may want to insert it as a sibling instead of as a block directly. You can use the `respondInline` option to insert the content at the same depth as the `from` position.

```ts
editor.commands.setContent('<p>123</p>')
editor.commands.streamContent(
  4,
  async ({ write }) => {
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 10)
    })
    write({ partial: '<p>hello ' })
    await new Promise((resolve) => {
      setTimeout(() => resolve(), 10)
    })
    write({ partial: 'world</p><p>ok</p>' })
  },
  { respondInline: true },
)
// Output: <p>123hello world</p><p>ok</p>
// As opposed to: <p>123</p><p>hello work</p><p>ok</p> when `respondInline` is `false`
```

## Technical details

Here is the full TypeScript definition for the `streamContent` command:

```ts
declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    streamContent: {
      streamContent: (
        /**
         * The position to insert the content at.
         */
        position: number | Range,
        /**
         * The callback to write the content into the editor.
         */
        callback: (options: StreamContentAPI) => Promise<any>,
        /**
         * The options to pass to the `insertContentAt` command.
         */
        options?: {
          parseOptions?: NonNullable<
            Parameters<RawCommands['insertContentAt']>['2']
          >['parseOptions']
          /**
           * This will insert the content at the same depth as the `from` position.
           * Effectively, this will insert the content as a sibling of the node at the `from` position.
           * @default true
           */
          respondInline?: boolean
        },
      ) => ReturnType
    }
  }
}

type StreamContentAPI = {
  /**
   * The function to write content into the editor.
   */
  write: (ctx: {
    /**
     * The partial content of the stream to insert.
     */
    partial: string
    /**
     * This function allows you to transform the content before inserting it into the editor.
     * It must return a Prosemirror `Fragment` or `Node`.
     */
    transform?: (ctx: {
      /**
       * The accumulated content of the stream.
       */
      buffer: string
      /**
       * The current partial content of the stream.
       */
      partial: string
      editor: Editor
      /**
       * Allows you to use the default transform function.
       */
      defaultTransform: (
        /**
         * The content to insert as an HTML string.
         * @default ctx.buffer
         */
        htmlString?: string,
      ) => Fragment
    }) => Fragment | Node | Node[]
    /**
     * Allows you to append commands to the chain before it is executed.
     */
    appendToChain?: (chain: ChainedCommands) => ChainedCommands
  }) => {
    /**
     * The buffer that is being written to.
     */
    buffer: string
    /**
     * The start of the inserted content in the editor.
     */
    from: number
    /**
     * The end of the inserted content in the editor.
     */
    to: number
  }
  /**
   * A writable stream to write content into the editor.
   * @example fetch('https://example.com/stream').then(response => response.body.pipeTo(ctx.getWritableStream()))
   */
  getWritableStream: () => WritableStream
}
```
