# Authoring content (https://systemhub.com/docs/concepts/authoring-content)

> Part of systemHUB Docs · Connect: Settings → AI Gateway in systemHUB · Auth: OAuth (scoping is workspace-level today — see Getting started) · MCP tools: 48 · Full corpus: https://systemhub.com/llms-full.txt

An agent that can call `create_system` is not yet an agent that produces *good* systemHUB documents. This page is the authoring contract: which HTML survives, which is silently changed, and the conventions that make a document genuinely usable. It is extracted from the live editor configuration (a custom CKEditor 5 build) and verified by round-trip audit — not guessed.

## The rule that matters most

> 🚨 **Send raw HTML, never entity-escaped HTML.** Write `<h2>Title</h2>`, not `&lt;h2&gt;Title&lt;/h2&gt;`. The API stores escaped content verbatim and returns success — but the document renders as visible tag soup (literal `<h2>` characters on screen). Nothing warns you. Escaping HTML while serialising into JSON is a classic agent failure mode: if your framework auto-escapes, unescape before sending, and read the document back after writing to confirm it rendered.

**The MCP/REST layer stores your HTML verbatim. The editor is the filter.** Content outside the supported set is not rejected at write time — it is silently removed or downgraded the first time a human opens the document in the editor and saves. No error is raised. So author *within* the supported set, or your content will change under the user's feet later.

## The four content fields

Every system, policy, and training document has four content areas. All except Title are rich text with the same formatting rules:

| Field | API parameter | Purpose |
|---|---|---|
| Title | `title` | Plain text, max 255 chars. |
| Overview | `description` | One or two sentences: what this is and why it matters. Not the steps. |
| Details (body) | `content` | The main content. Steps live here. |
| Notes | `note` | Internal notes for editors and reviewers. |

## Supported formatting (survives the editor)

- **Headings:** `<h1>`, `<h2>`, `<h3>` **only**. Use one `<h3>` per step.
- **Inline:** `<strong>`, `<i>`, `<u>`, `<s>`.
- **Highlights:** exactly four — `<mark class="marker-yellow">`, `marker-green`, `marker-blue`, `marker-pink`.
- **Text colour / font:** inline styles only — `style="color:#e74c3c"`, `font-size`, `font-family` (arbitrary values allowed).
- **Alignment / indent:** inline styles only — `style="text-align:center|right|justify"`, `style="margin-left:40px"` (40px steps).
- **Lists:** `<ul>` / `<ol>`, nesting fine. Checklists: `<ul class="todo-list"><li><label class="todo-list__label"><input type="checkbox" disabled><span class="todo-list__label__description">Task</span></label></li></ul>` (add `checked` for ticked).
- **Blocks:** `<blockquote>`, `<hr>`, code blocks `<pre><code class="language-python">` (24 languages incl. bash, javascript, sql, json, yaml).
- **Links:** `<a href>` — every link is forced to open in a new tab (`target="_blank"` added automatically).
- **Tables:** full support incl. header rows, `colspan`/`rowspan`, cell background colour, captions.
- **Video embeds:** paste the plain share URL, or write `<figure class="media"><oembed url="https://www.loom.com/share/abc"></oembed></figure>`. Auto-embed providers: YouTube, Vimeo, Wistia, Loom, Google Drive, ScribeHow, Pickaxe, Zoom clips, SharePoint, Dropbox mp4. NOT supported: Instagram, Twitter/X, Facebook, Maps, Flickr.
- **Emoji and unicode:** all fine.

## Silently stripped (do not use)

| Don't write | What happens / do instead |
|---|---|
| `<h4>` `<h5>` `<h6>` | Downgraded to plain paragraph. Only H1–H3 exist. |
| Inline `<code>` | Removed, text kept. Use a `<pre><code>` block instead. |
| `<sup>` / `<sub>` | Removed entirely. |
| `<mark>` without a `marker-*` class, or `pen-*` classes | Removed. Only the 4 markers survive. |
| `background-color` on a `<span>` | Removed. Use a marker highlight. |
| Formatting via CSS class (e.g. `class="text-center"`) | Ignored. Inline `style` only. |
| External images `<img src="https://…">` | **Removed entirely.** Images must be uploaded through the editor UI — the API cannot inject images. Mark the spot instead: `<p><mark class="marker-yellow">[Upload screenshot of the dashboard here]</mark></p>` |

**Auto-normalised (harmless):** `<b>`→`<strong>`, `<em>`→`<i>`, `<del>`→`<s>`; raw `<iframe>` becomes a responsive media wrapper.

## Structured fields are not body content

Email templates and video/media lists are structured data, not HTML:

- **Templates** (`{title, subject, details}`) — use `add_document_templates` / `edit_document_templates` / `remove_document_templates`.
- **Videos and media** (`{title, videoLink}`) — use `add_document_videos_and_media` / `remove_document_videos_and_media`.

Do not jam these into the `content` HTML; use the dedicated tools so they render in the app's Templates and Video & Media sections.

## Attachments: use `fileUrl`, not base64

`add_document_attachments` takes either `contentBase64` **or** `fileUrl` (a public HTTP(S) URL it fetches server-side). Prefer `fileUrl` for anything beyond a trivial file, and always when attaching in bulk — base64 sends the whole file through the tool call, so an agent pays for the content twice (once reading it, once writing it into the call). Any public URL works: a Drive share link, an S3 object, a static host. Attaching a dozen files by URL is a dozen two-line calls.

Two behaviours worth knowing before you script against it:

> **Attachments cannot be replaced.** Uploading a file whose name already exists on the document returns `skipped` with `reason: "duplicate"` and **leaves the original content in place** — it is not an error and it is not an overwrite. To ship a corrected file you must either use a different filename or delete the old attachment in the app, since the MCP has no delete.

- **Supported types are documents only**: PDF · `.doc/.docx/.docm` · `.xls/.xlsx/.xlsm` · `.ppt/.pptx/.pptm` · `.txt/.csv/.rtf` · `.md`. Anything else returns `skipped` with `reason: "invalid_type"` — **`.zip` is rejected**, so a multi-file kit cannot be attached as an archive. Images, MP3 and MP4 must be uploaded in the app.
- **Download URLs are public and unauthenticated.** The returned `downloadUrl` is a plain S3 object that anyone with the link can fetch, with no login. The path is UUID-based, so it is unguessable, but that is obscurity rather than access control — treat a leaked URL as permanent public read, and do not attach sensitive material to a document whose links get shared around.

## Size limit

Edits above roughly **126KB of content can fail with a 502**. `edit_*` replaces the entire `content` field on every call, so building a long document incrementally makes this *worse* — each edit resends everything written so far. Split genuinely large content across multiple documents instead.

## The anatomy of a good system (the SYSTEMology house format)

A real system answers four questions in order — *when does this run, what are the steps, how do you know it worked, and what does it look like done well*. Structure every draft as:

1. **Overview = context** (`description` field): what this system is and why it matters, 1–2 sentences.
2. **Trigger** — the first thing in the body: *when do you run this?* The event, schedule, or signal that kicks the system off ("When a new contract is signed…", "Every Monday before 10am…"). A system without a trigger never gets run.
3. **Steps** — numbered, each as an `<h3>`, action-first ("Send the welcome email", not "Welcome email"), in the order someone actually does the work.
4. **Definition of done** — the last section: *how do you know it worked?* The observable outcome ("The client has received the welcome email and the kickoff call is in both calendars"). Without it, "done" means "I stopped".

And the craft rules around that skeleton:

- **Yellow-highlight every genuine gap** the owner must fill (a name, link, threshold): `<p><mark class="marker-yellow">[Add the exact email template link here]</mark></p>`. Never invent specifics.
- **End with a "record it being done" note** — the best version of a system is screen-recorded while someone runs it, then refined.
- **First drafts get state ORANGE (`state: 1`, needs review)** so the owner knows to check and green-light it.

## Quick example

What an unbriefed agent writes (half disappears on first human save):

```html
<h4>Step 1</h4>
<p>Open the <code>CRM</code> and click <span style="background-color:yellow">New Deal</span>.</p>
<img src="https://example.com/screenshot.png">
```

systemHUB-ready:

```html
<h3>1. Create the new deal</h3>
<p>Open the CRM and click <strong>New Deal</strong>.</p>
<p><mark class="marker-yellow">[Upload a screenshot of the New Deal button here]</mark></p>
```

Next: **[Tool reference →](/docs/tool-reference)**
