Writing AI Assistant prompts

The editor never sends a full instruction to the model. It sends an action id (shorter, summarize, translate, …), optional extras (language, tone, prompt), and document blocks as JSON. Your backend loads a template for that action, substitutes placeholders, and calls the model.

This guide explains how to write those templates so each request type returns JSON the editor can apply, review, or show as a report.

Related: Issues by goal · Backend examples · Async jobs · AI report HTML framework · Assistant plugin reference

Mental model#

Layer Who owns it
Action id + mode + blocks Editor → your HTTP body
Prompt text / system rules Your template file (or builder)
Model JSON Model → you parse it
Shape for the editor You map model JSON → { issues } or { title, tab, html, … }

One file per action is enough: prompts/shorter.txt, prompts/summarize.txt, and so on. Load by body.action.

Context blocks (input)#

Every useful template describes the same input shape. Put this near the top of each prompt:

You receive document blocks as JSON: `{uid, type, tag, content? | items?}`.
- `type`: `heading`, `text`, or `list`.
- `tag`: `h1`–`h6`, `p`, `ul`, or `ol`.
- `content`: inner HTML for heading/text blocks.
- `items`: string array — one entry per top-level list item.

Then end with the document:

### Input:
[JSON]

[JSON] is replaced on the server with pretty-printed context.blocks.

Common placeholders

Placeholder Source
[JSON] context.blocks (preferred for structured edits)
[UI_LANGUAGE] lang — UI locale for labels / explanations
[LANGUAGE] language — target language for Translate
[TONE] tone — Change tone submenu
[USER_PROMPT] / [PROMPT] Free-form Ask AI text
[ACTION] / [MODE] / [SCOPE] Request metadata
[ISSUE_TYPE], [ORIGINAL_HTML], … Refine / Rephrase inside the review card

Unknown placeholders are left as-is. Keep names stable so one renderer works for all templates.

Modes → what the model must return#

The editor mode decides how the response is used. Your prompt must ask for the matching JSON shape (or a model shape you convert server-side for reports).

Mode Typical actions Model should return Editor gets
review (default) improve, shorter, fix, translate, … { "issues": [ … ] } Review cards (accept / reject)
apply better-wording { "issues": [ … ] } (+ annotations) Applied immediately
report summarize, key-points, meta-description, … Action-specific object You build { title, tab, html, data }
report-review check-writing { "report": {…}, "issues": […] } Report tab + review panel

Never ask the model for a full HTML document rewrite. Work block-by-block with uid / id.

Edit / rewrite (mode: review)#

Goal: change existing blocks. Each changed block becomes one issue.

Rules that belong in every edit prompt:

  • Only include blocks that actually change.
  • Preserve tag unless you intentionally change list kind.
  • Return inner HTML in content (no wrapping <p> / <h2> in the string).
  • For lists use items, not content.
  • JSON only — no markdown fences, no commentary.
  • Empty result: { "issues": [] }.

Example — Improve writing

You are a writing assistant embedded in a rich-text editor.
You receive document blocks as JSON: `{uid, type, tag, content? | items?}`.
- `type`: `heading`, `text`, or `list`.
- `tag`: `h1`–`h6`, `p`, `ul`, or `ol`.
- `content`: inner HTML for heading/text blocks.
- `items`: string array — one entry per top-level list item.

Rewrite the provided blocks to improve clarity and flow while preserving meaning.

Respond with a single JSON object:
{
  "issues": [
    {
      "id": "<uid of the block you are modifying>",
      "type": "Rewrite",
      "description": "<short explanation, one sentence>",
      "tag": "<same as input>",
      "content": "<new inner HTML for heading/text>"
    }
  ]
}

For `list` blocks use `items` instead of `content`.

Rules:
- Only include blocks that actually change.
- No markdown, no code fences — JSON only.
- If nothing should change, return {"issues": []}.

Respond in the user's UI language when possible: [UI_LANGUAGE].

### Input:
[JSON]

Example — Translate (uses [LANGUAGE])

Submenu items send action: "translate" plus language: "Swedish". The template must require that field:

Translate the provided blocks into the [LANGUAGE] language. Preserve inline markup where possible.

Respond with a single JSON object:
{
  "issues": [
    {
      "id": "<uid>",
      "type": "Translate",
      "tag": "<same as input>",
      "content": "<translated inner HTML>"
    }
  ]
}

For `list` blocks use `items` instead of `content`.
…

### Input:
[JSON]

Same pattern for Change tone with [TONE].

Example — Continue writing

Ask for the full revised block (original + continuation), not a fragment to append:

Continue the provided block naturally from where it ends.
Return the full revised inner content for that block (original plus continuation).
…

Insert new blocks (type: "add")#

Goal: propose headings, lists, or other blocks before an existing uid. Used by Headings, Enrich structure, and similar generate actions.

Critical fields:

Field Meaning
type Must be "add" (or "insert")
id uid of the block before which to insert
block heading, text, or list
tag e.g. h2, p, ul
content / items New block body

Example — Suggest headings

Analyze the document and propose new heading blocks where they improve structure.
Do not rewrite existing blocks — only suggest headings to insert.

Respond with:
{
  "issues": [
    {
      "id": "<uid of the block to insert before>",
      "type": "add",
      "description": "<why this heading belongs here>",
      "block": "heading",
      "tag": "h2",
      "content": "Section title"
    }
  ]
}

Rules:
- Use `type: "add"` for every issue.
- Prefer a logical heading hierarchy.
- If none are needed, return {"issues": []}.
- JSON only.

### Input:
[JSON]

Free-form Ask AI (action: prompt)#

The user types a request. Put it in a dedicated section so the model does not confuse instructions with content:

Apply the user's request to the provided blocks.

Respond with { "issues": [ … ] }  (same rewrite contract as Improve).

### User request:
[USER_PROMPT]

### Input:
[JSON]

Keep the issues contract identical to edit actions so one response path works for presets and Ask AI.

Insights / reports (mode: report)#

Goal: structured insight data — not block rewrites. The model returns an action-specific object. Your server owns title, tab, and HTML for the report tab; the model should not invent those. For richer layouts, build html with the AI report HTML framework (rx-ai-report / --rx-report-*).

Action Ask the model for
summarize { "summary": ["paragraph 1", "paragraph 2"] }
key-points { "keyPoints": [{ "point", "comment" }] }
meta-description { "metaDescription": "…" } (plain text, ~120–158 chars)
preview-text / brainstorm-titles Action-specific fields (see your templates)

Example — Summarize

Summarize the provided content concisely while preserving key facts.

Respond with a single JSON object:
{
  "summary": [
    "<plain text paragraph 1>",
    "<plain text paragraph 2>"
  ]
}

Rules:
- `summary` must be an array of plain-text paragraphs
- Use 1–5 paragraphs depending on length
- Language must match the input content
- No HTML, no markdown — JSON only

### Input:
[JSON]

The model must not return title / tab / html. Your server builds those from the structured fields.

1. Model reply (what the prompt asks for):

{
  "summary": [
    "Strong onboarding in the first two weeks builds trust; a vague start signals that process is optional.",
    "Remote teams need written material and a predictable rhythm more than extra meetings."
  ]
}

2. What you send to the editor (after your formatter runs):

{
  "title": "Summary",
  "tab": "summary",
  "html": "<div class=\"rx-ai-report\">\n<section class=\"rx-ai-report__content\">\n<p>Strong onboarding in the first two weeks builds trust; a vague start signals that process is optional.</p>\n<p>Remote teams need written material and a predictable rhythm more than extra meetings.</p>\n</section>\n</div>",
  "data": {
    "summary": [
      "Strong onboarding in the first two weeks builds trust; a vague start signals that process is optional.",
      "Remote teams need written material and a predictable rhythm more than extra meetings."
    ]
  }
}
Field Source
title, tab Your server map for the action (summarize → Summary / summary)
html Built from summary[] with the AI report HTML framework (escape text, wrap in .rx-ai-report)
data Raw model object, optional for clients that want the structured fields

The editor opens a host tab with title and renders html. It never reads summary from the top level.

Example — Meta description

Write an SEO meta description for the provided content.

Respond with:
{
  "metaDescription": "<plain text meta description>"
}

Rules:
- Length: 120 to 158 characters
- Plain text only
- Language matches the content
- No HTML, no markdown — JSON only

### Input:
[JSON]

Again, the model returns only the structured field. Your server owns the report shell.

1. Model reply:

{
  "metaDescription": "Learn how clear onboarding in the first two weeks builds trust for remote teams — with checklists, buddies, and async docs that keep day one productive."
}

2. What you send to the editor:

{
  "title": "Meta description",
  "tab": "meta-description",
  "html": "<div class=\"rx-ai-report\">\n<section class=\"rx-ai-report__section\">\n<div class=\"rx-ai-report__panel\">\n<div class=\"rx-ai-report__label\">Meta description</div>\n<p>Learn how clear onboarding in the first two weeks builds trust for remote teams — with checklists, buddies, and async docs that keep day one productive.</p>\n<div class=\"rx-ai-report__callout is-positive\">\n<div class=\"rx-ai-report__callout-title\">Length</div>\n<p class=\"rx-ai-report__callout-text\">152 characters</p>\n</div>\n</div>\n</section>\n</div>",
  "data": {
    "metaDescription": "Learn how clear onboarding in the first two weeks builds trust for remote teams — with checklists, buddies, and async docs that keep day one productive."
  }
}
Field Source
title, tab Server map (meta-description → Meta description / meta-description)
html Escaped metaDescription inside .rx-ai-report (panel + length callout; is-warning if outside 120–158)
data Raw model object

Check writing (mode: report-review)#

Goal: one response with both a teaching report and optional rewrite issues. The editor opens a report tab and the review panel.

Example prompt

You are an editorial mentor embedded in a rich-text editor.
You receive document blocks as JSON: `{uid, type, tag, content? | items?}`.

Teach the author through the text — do not dump a full rewrite in the report.
Be precise and honest. Put concrete rewrites only in `issues`.

In the report, illustrate principles with independent weak/strong example pairs —
do not paste the user's paragraph back as the “strong” version.

Respond with a single JSON object:
{
  "report": {
    "textType": "<what kind of text this is>",
    "readingImpression": "<why it feels weak or strong when reading>",
    "visibleHabits": ["<habit visible in the text>"],
    "principles": ["<simple principle that would help>"],
    "distinctions": [
      { "label": "<e.g. real observation vs generic statement>", "observation": "<what you see>" }
    ],
    "examples": [
      {
        "principle": "<short principle>",
        "weak_pattern": "<independent weak example>",
        "strong_pattern": "<independent strong example>"
      }
    ],
    "actions": ["<short actionable suggestion>"]
  },
  "issues": [
    {
      "id": "<uid of a block that should change>",
      "type": "<short label, e.g. Clarity>",
      "description": "<one sentence>",
      "tag": "<same as input>",
      "content": "<revised inner HTML>"
    }
  ]
}

Rules:
- JSON only — no markdown, no code fences.
- Only include issues for blocks that truly need a change.
- If nothing should change, return issues: [].

Respond in the user's UI language when possible: [UI_LANGUAGE].

### Input:
[JSON]

From model JSON to editor response

1. Model reply (structured report + optional issues):

{
  "report": {
    "textType": "Practical guide on employee onboarding for remote teams.",
    "readingImpression": "Useful ideas sit next to overloaded sentences — the reader has to work to find the advice.",
    "visibleHabits": [
      "One paragraph piles clauses until the point disappears.",
      "Spelling slips undercut an otherwise practical checklist."
    ],
    "principles": [
      "One concrete idea per paragraph.",
      "Prefer plain wording over formal padding."
    ],
    "distinctions": [
      {
        "label": "real observation vs generic statement",
        "observation": "The opening names “two weeks” but does not show what the new hire actually experiences."
      }
    ],
    "examples": [
      {
        "principle": "Say the delay, not the jargon",
        "weak_pattern": "The onboarding cadence frequently devolves into a synchronous bottleneck.",
        "strong_pattern": "Onboarding often stalls because new hires wait a day for answers that should have been written down."
      }
    ],
    "actions": [
      "Simplify the densest paragraph before sharing the guide."
    ]
  },
  "issues": [
    {
      "id": "demo-p-complex",
      "type": "Simplify",
      "description": "The sentence is too dense; the advice gets lost in formal wording.",
      "tag": "p",
      "content": "Even when a company prefers async communication, onboarding often stalls because new hires wait for live answers from people in other time zones."
    }
  ]
}

2. What you send to the editor:

{
  "title": "Writing check",
  "tab": "check-writing",
  "html": "<div class=\"rx-ai-report\"><!-- header, habits, principles, findings, compare, steps — see AI report HTML framework --></div>",
  "issues": [
    {
      "id": "demo-p-complex",
      "type": "Simplify",
      "description": "The sentence is too dense; the advice gets lost in formal wording.",
      "tag": "p",
      "content": "Even when a company prefers async communication, onboarding often stalls because new hires wait for live answers from people in other time zones."
    }
  ],
  "data": { }
}

Put the entire model reply from step 1 into data (including its report and issues). html is built only from report sections — escape text and wrap each part in your report markup. Top-level issues are the same array the model returned, passed through for the review cards.

Field Source
title, tab Server map (check-writing → Writing check / check-writing)
html Rendered from report.* with the AI report HTML framework (not from issues)
issues Same array the model returned — block rewrites for Accept / Reject
data Raw model object

Better wording (mode: apply)#

Goal: mark key phrases and attach alternatives. Used with the Annotations plugin. Unlike report actions, there is no title / tab / html shell — the editor applies { issues } immediately.

Example prompt

You are a writing assistant embedded in a rich-text editor.
You receive document blocks as JSON: `{uid, type, tag, content?}`.

For each block, find words and phrases that carry key meaning (not every word).
Wrap each such phrase in <span data-block="wording">original phrase</span>.
For every wrapped phrase, prepare 2–3 alternative wordings.

Respond with a single JSON object:
{
  "issues": [
    {
      "id": "<uid of the block>",
      "type": "Better wording",
      "description": "Alternative wordings for key phrases",
      "tag": "<same as input>",
      "content": "<inner HTML with wording spans>",
      "annotations": [
        ["alternative 1", "alternative 2", "alternative 3"]
      ]
    }
  ]
}

Rules:
- `annotations` order MUST match span order in `content` (first span → first array).
- Each inner array: 2–3 non-empty alternative strings.
- Only `data-block="wording"` on spans — no `data-ref`, class, or other attributes.
- Return inner content only — do not wrap in block tags.
- Only include blocks where at least one phrase was marked.
- JSON only — no markdown, no code fences.
- If nothing should change, return {"issues": []}.

Respond in the user's UI language when possible: [UI_LANGUAGE].

### Input:
[JSON]

From model JSON to editor response

Suppose the input block was:

{
  "uid": "p1",
  "type": "text",
  "tag": "p",
  "content": "Most texts on the internet go unnoticed."
}

1. Model reply:

{
  "issues": [
    {
      "id": "p1",
      "type": "Better wording",
      "description": "Alternative wordings for key phrases",
      "tag": "p",
      "content": "Most texts on the internet go <span data-block=\"wording\">unnoticed</span>.",
      "annotations": [
        ["ignored", "overlooked", "missed"]
      ]
    }
  ]
}

How the arrays line up:

Span order in content Matching annotations[i]
1st <span data-block="wording">unnoticed</span> ["ignored", "overlooked", "missed"]

With two spans, you need two inner arrays — same order:

{
  "content": "The <span data-block=\"wording\">Review group</span> inspects the draft and proposes changes you can <span data-block=\"wording\">accept or reject</span>.",
  "annotations": [
    ["review panel", "feedback set", "editing pass"],
    ["approve or dismiss", "keep or discard", "take or skip"]
  ]
}

2. What you send to the editor:

For mode: "apply", return the parsed model object as-is (after validation). Do not wrap it in a report envelope:

{
  "issues": [
    {
      "id": "p1",
      "type": "Better wording",
      "description": "Alternative wordings for key phrases",
      "tag": "p",
      "content": "Most texts on the internet go <span data-block=\"wording\">unnoticed</span>.",
      "annotations": [
        ["ignored", "overlooked", "missed"]
      ]
    }
  ]
}
Field Role
issues[].id Block uid to update
issues[].content New inner HTML with wording spans
issues[].annotations Parallel list of alternatives per span
title / tab / html Not used for this mode

The editor applies the new content, turns each wording span into an annotation callout, and fills choices from the matching annotations entry. Your server only needs to parse JSON, reject malformed replies, and return { "issues": […] }.

Writing quality checklist#

Name the JSON shape explicitly

Paste a minimal example object in the prompt so the model knows the exact keys to return.

Forbid markdown fences

Models often wrap JSON in ```json fences. Tell them not to — otherwise your parser will fail.

Tie outputs to uid

Every rewrite or add issue needs a stable id that matches a block in the document.

Separate “what to do” from “input”

Use clear headings such as ### Input: or ### User request: so instructions and document content do not blend together.

Match language

Write content in the same language as the source text. Use [UI_LANGUAGE] for UI-facing labels and explanations.

Handle empty work

Always allow an empty result — { "issues": [] } for edits, or an empty report field set when nothing useful can be said.

One action, one job

Do not overload improve with summarization. Add a new action (and template) instead.

Test with real blocks

Include a list block and a heading in your fixtures. Many prompts forget items and only work for plain paragraphs.

Mapping actions to templates#

Menu / action id Suggested template Mode
improve, shorter, longer, fix, simplify, rephrase, … { issues } rewrite review
translate + language rewrite + [LANGUAGE] review
tone + tone rewrite + [TONE] review
prompt (Ask AI) rewrite + [USER_PROMPT] review
headings, enrich-structure { issues } with type: "add" review
summarize, key-points, meta-description, … insight object report
check-writing { report, issues } report-review
better-wording issues + annotations apply

Ship starter templates next to your endpoint (one .txt per action). Adapt wording for your product and model, but keep the JSON contracts stable so the editor UI stays predictable.

Next: wire these templates into a working API — AI Assistant backend examples.