Skip to content
Docxcelerate

Nodes

Paragraph

Prose, rendered from your data or resolved from prompts at request time.

The workhorse. A static paragraph returns a string from your typed data; a dynamic one carries prompts and a placeholder, and is filled at request time. Both land as the same node kind, differing only by `mode`.

Helpers
paragraph
Node kind
paragraph
Category
Text
Resolves
Both
Children
None. Paragraphs are leaves.
Option Type What it does
id required string Stable address for the node. Generation endpoints target it and build artifacts diff on it, so treat a rename as a breaking change.
render required (data, availableTokens) => string Static only. Receives your typed data and the token budget, returns the text. May be async.
placeholder (data, availableTokens) => string Dynamic only. What previews show in place of generated content. Optional, but a letter that reads badly without one cannot be reviewed.
generalPrompt required (data, availableTokens) => string Dynamic only. What this node should say.
infoPrompt (data, availableTokens) => string Dynamic only. Context the model should have but should not restate.
negativePrompt (data, availableTokens) => string Dynamic only. What to avoid — claims, tones, or facts it must not invent.
systemPrompt (data, availableTokens) => string Dynamic only. Role and voice, applied ahead of the other prompts.
derivers DeriverInvocation[] Values computed before the node resolves, written to derived.* and readable from a template token. Built with derive().

Writing one

render receives your typed data and the token budget, and returns a string. There is no template language between them, so formatting, branching and pluralisation are all ordinary TypeScript:

import { paragraph } from "docxcelerate";
import type { MemberData } from "../types.ts";

export const Greeting = paragraph<MemberData>({
  id: "greeting",
  render: (data) => `Dear ${data.memberName},`,
});

It may be async — but a node that fetches is a node that can fail mid-build, so prefer putting the value in your data first.

The token budget

The second argument to render, placeholder and every prompt is availableTokens: the budget this node was allotted, 2000 unless you set availableTokens in the build options. Static nodes can ignore it. Dynamic ones should spend it:

generalPrompt: (data, availableTokens) =>
  `Explain the change. At most ${Math.floor(availableTokens / 4)} words.`,

Branching belongs in the render

A document that says one of three things is one node with three outcomes, not three nodes behind conditions. The id stays put, the tree keeps its shape whoever the recipient is, and the logic is code you can test.

Dynamic paragraphs

A dynamic paragraph swaps render for prompts and a placeholder. Preview builds resolve it to the placeholder and label it; request-time builds send the prompts. Only generalPrompt is required — the fourth variant below shows what the other three are for.

Variants

Static

src/nodes/paragraph/static.node.ts

Data in, a line of text out.

import { paragraph } from "docxcelerate";
import type { SampleData } from "../sample-data.ts";

/** The smallest useful node: an id, and a render that turns data into a line. */
export const Greeting = paragraph<SampleData>({
  id: "greeting",
  render: (data) => `Dear ${data.memberName},`,
});
paragraph · static Open ↗
What it resolves to

The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.

{
  "id": "greeting",
  "kind": "paragraph",
  "mode": "static",
  "text": "Dear Adaeze Nkemelu,"
}

Static, with branching

src/nodes/paragraph/conditional.node.ts

One node, several outcomes — and the id stays put.

import { paragraph } from "docxcelerate";
import { money, type SampleData } from "../sample-data.ts";

/**
 * Branching lives in the render, not the template: one node and one id,
 * whichever branch a member falls down.
 */
export const PriceChange = paragraph<SampleData>({
  id: "price-change",
  render: (data) => {
    const delta = data.newPrice - data.lastPrice;

    if (delta === 0) {
      return `Your ${data.plan} membership renews at ${money(data.newPrice)} ` +
        `a year — the same price you paid last year.`;
    }

    const direction = delta > 0 ? "rising" : "falling";
    const percent = Math.abs((delta / data.lastPrice) * 100).toFixed(1);

    return `Your ${data.plan} membership is ${direction} by ${percent}%, from ` +
      `${money(data.lastPrice)} to ${money(data.newPrice)} a year. That is ` +
      `${money(data.newPrice / 12)} a month from ${data.renewsOn}.`;
  },
});
paragraph · static, with branching Open ↗
What it resolves to

The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.

{
  "id": "price-change",
  "kind": "paragraph",
  "mode": "static",
  "text": "Your Peak Anytime membership is rising by 5.1%, from £468 to £492 a year. That is £41 a month from 1 October 2026."
}

Dynamic

src/nodes/paragraph/dynamic.node.ts

A prompt and a placeholder. Previews show the placeholder, labelled.

import { paragraph } from "docxcelerate";
import type { SampleData } from "../sample-data.ts";

/**
 * The minimum a dynamic paragraph needs: one prompt, and a placeholder so the
 * preview still reads as a letter.
 */
export const NextSteps = paragraph<SampleData>({
  id: "next-steps",
  placeholder: (data) =>
    `Your membership renews automatically on ${data.renewsOn}. ` +
    `Nothing is needed from you unless you want to change plan.`,
  generalPrompt: (data) =>
    `In two sentences, tell ${data.memberName} that their membership renews ` +
    `automatically on ${data.renewsOn} and how to change plan before then.`,
});
paragraph · dynamic Open ↗
What it resolves to

The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.

{
  "id": "next-steps",
  "kind": "paragraph",
  "mode": "dynamic",
  "text": "Your membership renews automatically on 1 October 2026. Nothing is needed from you unless you want to change plan."
}
What the endpoint is asked

Resolved against the same sample data. A preview build stops at the placeholder; a request-time build sends these.

general
In two sentences, tell Adaeze Nkemelu that their membership renews automatically on 1 October 2026 and how to change plan before then.

Dynamic, all four prompts

src/nodes/paragraph/prompted.node.ts

System, general, info and negative, each doing one job.

import { paragraph } from "docxcelerate";
import { money, type SampleData } from "../sample-data.ts";

/**
 * All four slots: general says what to write, info supplies facts without
 * asking for them back, negative fences off the failure modes, system fixes
 * the voice. `availableTokens` is the budget the build allotted this node.
 */
export const Apology = paragraph<SampleData>({
  id: "pool-closure",
  placeholder: () =>
    `The main pool is closed for resurfacing until 12 October. ` +
    `The teaching pool and all land-based classes are running as normal.`,
  systemPrompt: () =>
    `You write for a public leisure centre. Plain British English, second ` +
    `person, no marketing language.`,
  generalPrompt: (data, availableTokens) =>
    `Apologise to ${data.memberName} for the main pool closure and say what ` +
    `is still open. At most ${Math.floor(availableTokens / 4)} words.`,
  infoPrompt: (data) =>
    `The main pool at ${data.centreName} is resurfacing until 12 October. ` +
    `The teaching pool, gym and classes are unaffected. Members on ` +
    `${data.plan} paying ${money(data.newPrice)} a year get two guest passes ` +
    `as compensation.`,
  negativePrompt: () =>
    `Do not promise a refund, do not give a reopening date beyond 12 October, ` +
    `and do not restate the price.`,
});
paragraph · dynamic, all four prompts Open ↗
What it resolves to

The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.

{
  "id": "pool-closure",
  "kind": "paragraph",
  "mode": "dynamic",
  "text": "The main pool is closed for resurfacing until 12 October. The teaching pool and all land-based classes are running as normal."
}
What the endpoint is asked

Resolved against the same sample data. A preview build stops at the placeholder; a request-time build sends these.

system
You write for a public leisure centre. Plain British English, second person, no marketing language.
general
Apologise to Adaeze Nkemelu for the main pool closure and say what is still open. At most 500 words.
info
The main pool at Riverside Leisure Centre is resurfacing until 12 October. The teaching pool, gym and classes are unaffected. Members on Peak Anytime paying £492 a year get two guest passes as compensation.
negative
Do not promise a refund, do not give a reopening date beyond 12 October, and do not restate the price.

Notes

  • Both modes resolve to kind: "paragraph". A renderer reads mode if it cares at all; it never branches on which helper you called.
  • An empty string returns an empty paragraph, not no node. To drop the node, leave it out of the tree.
  • Renderers escape the text. A paragraph cannot smuggle markup into the page, and is not the place for formatting.

Edit this page on GitHub ↗