Essentials
Using AI
Mark a node as one an engine writes, steer what it says, and decide who actually writes it.
Most nodes are written from your data. Some are better written fresh for each document — a summary, a covering note, an explanation that should sound different when an account is £12 overdue than when it is £1,200.
useAi marks a node as one of those. Nothing calls a model just because you
wrote it; the hook says this node gets written later, and the rest of this page
is about what “later” means.
The smallest AI node
import { Paragraph, useAi } from "docxcelerate/template";
export const Summary: Paragraph = () => {
useAi({
ask: "Two sentences summarising the repairs visit.",
placeholder: "A short summary of the visit.",
});
return <Paragraph id="summary" />;
};
Two fields, both required.
askis what the node should say.placeholderis what stands in its place until something has written it.
Notice the paragraph has no text of its own. That’s the whole difference: a node with text is written by you, a node with prompts is written for you. Give one element both and you get an error — Docxcelerate won’t guess which you meant.
Steering what it says
Four optional fields, each doing one job:
| Field | What it does |
|---|---|
voice | How it should sound |
from | The facts to write from |
avoid | What it must not say |
example | What a good answer looks like |
export const Summary: Paragraph = () => {
const [visit] = useState((data: RepairData) => data.visit);
useAi({
ask: "Two sentences telling the resident what was booked and what to expect.",
placeholder: "A short summary of the visit.",
voice: "A housing officer writing to a tenant. Plain English, second person.",
from: { trade: visit.trade, date: visit.date, window: visit.window },
avoid: "Do not invent a phone number or promise a time outside the window.",
example:
"An electrician is booked for Tuesday 3 March, between 9am and 1pm. " +
"Please make sure someone over 18 is at home for the whole window.",
});
return <Paragraph id="summary" />;
};
from takes your data, not a sentence about it
Hand over the values themselves:
from: { trade: visit.trade, date: visit.date, window: visit.window }
Don’t write them into a sentence first. The model would only have to pull the values back out of it again.
A plain string works too, when the facts are already written out:
from: "The last visit was cancelled by the contractor, not the resident."
example is the most useful field here
Telling a model what shape you want leaves it guessing. Showing it one gives it something to copy. An example locks down the parts you don’t want changing — how it opens, what order things come in, how long it is, how formal it sounds — so the model only fills in what genuinely differs between documents.
Write it as finished text, filled in the way a real one would be:
example: "An electrician is booked for Tuesday 3 March, between 9am and 1pm."
Not as a fill-in-the-blanks form. That’s just describing the shape again, and it gets treated that way:
example: "A [trade] is booked for [date], between [start] and [end]." // avoid
Pass an array when the answer genuinely comes out in different shapes. Two or more get numbered, and are read as a range to work from rather than one template to copy:
example: [
"An electrician is booked for Tuesday 3 March, between 9am and 1pm.",
"We could not book a slot this week. A coordinator will call you on Monday.",
]
Why the placeholder is required
Previews show the placeholder for every AI node, never generated text. That keeps a preview instant, free, the same every time, and entirely on your machine.
The placeholder is also what a reader sees if generation is skipped or fails. So it isn’t optional, and it’s worth a real sentence:
placeholder: "A short summary of the visit." // good
placeholder: "TODO" // this ships to somebody
Prompts as props
Short prompts read better written on the element itself. Each field has a matching prop:
| Hook field | Prop |
|---|---|
ask | generalPrompt |
voice | systemPrompt |
from | infoPrompt |
avoid | negativePrompt |
example | examplePrompt |
placeholder | placeholder |
<Paragraph
id="summary"
generalPrompt="Two sentences summarising the repairs visit."
placeholder="A short summary of the visit."
/>
A prop beats the hook, so you can always override what a shared hook set. That’s what makes a house style easy to share:
export function useHouseVoice() {
useSetPrompts({
systemPrompt: "You write for a housing association. Plain English, second person.",
});
}
The prompts land on whatever node the calling component returns. So any
component that calls useHouseVoice() gets that voice, and the hook never needs
to know which node it’s setting it on.
Pictures and charts, too
An <Image> given prompts instead of a src, or a <Graph> given prompts
instead of data, works exactly the same way:
<Image
id="cover"
generalPrompt="A quiet watercolour of a terraced street, no people."
negativePrompt="No text, no logos, no recognisable faces."
placeholder="Cover illustration."
width={160}
height={90}
/>
negativePrompt matters most here. Generated pictures go wrong in the same few
ways every time — words baked into the image, invented logos, faces you
recognise — and one line in the node rules them out.
Who actually writes the text
Two answers, and you pick by how you build.
You do, with a client you supply. Build with dynamicMode: "resolve" and an
aiClient. It’s a small interface — one required method:
import { buildDocument } from "docxcelerate";
const doc = await buildDocument(documentTemplate, data, {
dynamicMode: "resolve",
aiClient: {
generateParagraph: (request) => callYourModel(request.prompt),
},
});
request.prompt is all of the node’s prompts joined into one string.
request.prompts gives you the same thing as a list, split by kind — system,
general, info, negative, example — if you’d rather map them onto your
provider’s own fields. generateImage and generateGraph are optional: leave
them off and those nodes show their placeholders instead of failing the document.
To see what a node is asking for without asking anything, use the client that writes the prompt straight back:
import { EchoAiClient } from "docxcelerate";
await buildDocument(documentTemplate, data, {
dynamicMode: "resolve",
aiClient: new EchoAiClient(),
});
An engine does, per request. When you publish, nothing calls a model at build time. The node is stored with its prompts and no text, and the engine writes it fresh for every document it produces. See the engine.
What to remember
useAimarks a node; it doesn’t call anything by itself.- A node has text or prompts, never both.
- Previews always show the placeholder, so write one worth reading.
examplecosts one line and does more than any other field.
Next
- Paragraph — the node type most often written this way
- Image — dynamic pictures in full
- The engine — publishing, and writing per request