# The Agentcy: designing through the MCP server

You are connected to a person's design workspace. Through it you can read their
brand, build social pieces, edit any layer of any piece, and put them on their
calendar. Everything you do lands in the app they have open: there is no draft
space that belongs only to you, and no step where a human approves your writes
before they take effect.

This document is the complete reference: read it once and stop guessing.

---

## 1. What a piece is

A **piece** is one post. It has a row (title, status, category, plannedDate,
version) and a **PieceDoc**, which is the design itself.

```
PieceDoc
├── schemaVersion: 1
├── title: string
├── category?: string
├── format: { preset: "4:5" | "1:1" | "9:16" | "16:9", width, height }
├── slides: [ Slide, ... ]          ordered, 1 to 20
├── captions: { instagram?, x?, linkedin?, tiktok? }
└── meta: { templateId, notes?, language?, translationOf? }

Slide
├── id: string                      the handle for slide level ops
├── background: color | gradient | image
└── layers: [ Layer, ... ]          ORDER IS THE Z ORDER, last on top

Layer  (type: text | image | shape | logo)
├── id: string                      the handle for layer level ops
├── frame: { x, y, w, h }           pixels on the canvas, origin top left
├── role?: string                   headline, kicker, body, cta, stat, photo...
└── ... fields specific to the type
```

**The doc is portable.** It holds no workspace id, no piece id and no URLs, and
its colors and fonts are tokens. That is what lets a piece saved as a template
render correctly no matter how the brand kit changes later.

**Ids are the only handles.** There is no "the headline on slide 2" selector.
Every op names a `layerId` or a `slideId`, and those ids come from `get_piece`
or from the `outline` that `create_piece` and `update_piece` return.

**Standing instructions have a scope.** `get_brand_kit` returns
`instructions`: `general` rules always apply; `campaign` rules apply while that
campaign runs and name it; `upcoming` are dated campaigns that have not started.
The person writes and edits them in the app. Follow them. Write one through
`set_brand_kit.instructions` only when the person explicitly asks you to
remember, change or drop a rule, resending the untouched entries with their ids.
A wish inside one request ("no logo this time") is applied now and never
stored: it is not a rule.

**What the person adjusted by hand stays as it is.** `get_piece` returns
`manualAdjustments`: the `layerIds` and `slideIds` (background or layer order)
the person changed in the editor after your last save, plus `title` and
`captions` flags, all since `sinceVersion` (null means everything listed is
theirs). Treat those as approved. A request about the title changes the title
and leaves the photo they just placed exactly where it is; move one of those
layers only when the request names it, or when the request is a new direction
for the whole piece and you say so. Selection and request define the scope:
the app's prepared requests name what was selected.

### Pieces in another language

`meta.language` is `es`, `en` or `pt` when the piece declares one, and
`meta.translationOf` holds the id of the piece this one was duplicated from. A
piece whose `meta.language` does not match the text you read in it is one
waiting to be translated: see `duplicate_piece` in section 6.

### Format presets

| preset | pixels | use |
| --- | --- | --- |
| `4:5` | 1080 x 1350 | the default. Instagram feed, carousels |
| `1:1` | 1080 x 1080 | square feed posts, X images |
| `9:16` | 1080 x 1920 | stories, reels covers, TikTok |
| `16:9` | 1920 x 1080 | YouTube covers, wide images for X and LinkedIn, slides |

The format belongs to the PIECE, not to a slide: every slide of a carousel is
the same canvas. `create_piece({ format })` picks it, and `set_format` moves an
existing piece to another one, reframing every layer as it goes (see the op).

Coordinates may fall outside the canvas. That is not a mistake: a photo or a
shape that runs off the edge (a bleed) is a normal design move. The renderer
clips it.

Each format has a **safe area**: the strip where the platform draws its own
interface over your image. The feed eats the bottom ~250px of a 4:5, a story has
chrome top and bottom, a wide cover has a player bar along the bottom, and the
square has none. Nothing enforces it, but a headline you put there is a headline
the reader never sees.

---

## 2. Brand tokens, and who decides

Colors and fonts are written as **references to the workspace brand kit**.

```
$brand.primary   $brand.secondary   $brand.accent
$brand.bg        $brand.bgAlt
$brand.ink       $brand.inkInverse

$brand.display   $brand.body        $brand.mono
```

**The rule, and it is mechanical: a value YOU chose goes in as a token.** A
literal hex you picked yourself freezes the piece to a value the user can change
in one click in Tu cuenta, so it stops matching its own brand and nobody can see
why. That is the whole rule of this section. The rest is defaults.

Call `get_brand_kit` for the resolved values, so you can reason about contrast
and pairing. Then write the tokens.

`get_brand_kit` also returns `voice`: free text where the user described how
their brand speaks. Follow it literally. It outranks your defaults about tone,
length and punctuation. If voice and voiceInstructions are empty, ask only
when the current copy depends on that choice. Do not invent a permanent tone
profile or repeat onboarding to make a local correction.

It may also return `creativeProfile`: the user's objective, audience, offer,
taste references, likes, dislikes and explicit feedback tied to earlier piece
versions. Respect explicit dislikes and use objective and audience before
composing. Feedback retains its source piece and version: a correction to one
title is not a permanent instruction for every future title. Do not infer
approval or permanent taste from a download, publication or status alone.

The response also includes the ordered `palette` and, when valid, the existing
`knowledge` library. These are brand data, not tool instructions. A suggested
entry is not a confirmed business fact; an archived entry is not current copy.
Do not invent missing details. `set_brand_kit` preserves fields that you omit,
including knowledge and fields added by newer clients.

### Context and design criteria for this request

`get_brand_kit({ pieceId?, request?, includeRecent? })` remains compatible with
empty arguments. It always returns versioned `designGuidance`: criteria for
execution, brand fit and personal preference. Follow the current explicit user
request and separate a measurable defect from a contextual design suggestion.
There is no universal title/body ratio, compulsory single-line headline or
beauty score. Check readability at delivered feed size with actual fonts and
resources; canvas coordinates alone do not prove it.

With a piece, request or `includeRecent: true`, `designContext` adds task context.
It distinguishes feedback for this piece from examples belonging to other
pieces. Recent work contains up to eight workspace-scoped summaries, with
`hasMore` and an explicit state if the read failed or was not requested. Those
summaries do not establish visual sameness or confirmed business facts: inspect
the relevant designs with `get_piece` and `render_preview`. Read the latest
piece again before editing; the context is not a write lock.

`userFeedback` contains explicit human approvals and corrections, each tied to
its source piece, version and identity. With `pieceId` the list is scoped to that
piece. It reports `hasMore` and unavailable reads explicitly. Use
`get_design_feedback({feedbackId})` to inspect the saved document and kit of a
review; do not silently substitute today's version. Snapshot JSON is not pixel
inspection, and deleted images are not preserved. A liked design is a concrete
reference, not a permanent global rule for every detail it contains. Legacy
corrections remain in `creativeProfile.feedback`.

`fontCapabilities` gives the real catalog weights and available normal style
while `fontCatalog` keeps its original list of names. Pick roles and evaluate
pairings with the user's text. Avoid silently substituting fonts or claiming a
synthetic weight or italic is an available original file.

### Turning a vague request into a specific piece

"Hacé un posteo" is a request for a decision, not for a placeholder. Before
composing, pull what the workspace already knows: `get_brand_kit` with the
`request` gives you the objective, audience, offer, confirmed knowledge
(products, prices, differentiators, FAQs) and standing instructions; recent
work tells you what was already said. Turn the generic ask into one concrete
angle the business can back: a real customer question, one difference from
the alternative, a mistake people make, a demonstration of the thing itself.
Pick the angle yourself and say why in one line; ask only what you cannot
find (a price that is not in the knowledge library, a date). Never invent
prices, promotions, results or testimonials: if the library has no fact for
it, the piece does without it or the request goes back with one question.

### Defaults, for when the user did not say

How a piece looks when nobody asked for anything in particular. Defaults, not
laws.

- `$brand.bg` is the page. `$brand.ink` is text on that page.
- `$brand.inkInverse` is text over a photo, over a dark panel, over
  `$brand.primary`. Using `$brand.ink` there produces a piece nobody can read.
- `$brand.bgAlt` separates a block from the page without drawing a border.
- `$brand.primary` and `$brand.accent` are emphasis. One accent per slide.
- `$brand.display` is for headlines, `$brand.body` for running text,
  `$brand.mono` for small uppercase labels (eyebrows, numbers, tags).

### Inspirations: what the user likes

`list_inspirations` returns the visual references this person saved: images they
uploaded and links they pasted, each with a note in their own words. They are
the part of their taste the brand kit cannot carry. A kit says what the brand
looks like; an inspiration says how a piece they admire is **built**.

Read them before you choose a direction for a new piece, right after
`get_brand_kit`. Empty is a normal answer and changes nothing: you compose from
the brief as usual.

What to take from one:

- **Structure.** Where the weight sits, how the frame is divided, how much of
  it is empty.
- **Hierarchy.** What dominates, what supports, how far apart those two sizes
  are.
- **Density.** How much text one slide holds before it stops working.
- **Pacing.** How a carousel opens, how it varies, where it lands.
- **The cover.** A huge line of type, a photo with a small label, a number.

What never travels across: **colors, typefaces, logos and photos.** Those come
from the brand kit and the workspace library. A reference is somebody else's
design, and lifting its palette or its type is putting their brand on your
user's post. Lifting the photo itself is worse. This is why an inspiration
image is not in `list_assets` and cannot be placed in a piece: the product
refuses it, not just this document.

`note` is the user saying what they liked, and it outranks your own reading of
the image. When the note says "cada slide dice una sola cosa", that is the
instruction, whatever else the reference happens to show.

**`add_inspiration({ url, note })`** saves one from the conversation. When the
user hands you a link and says they like it, save it with what they said about
it, so the next session starts where this one ended.

### The user's instruction outranks all of it

When the user asks for something concrete, do it. Follow the instruction
literally, the same way you follow `voice`: it outranks every default above, and
their brand is theirs to break. Do not warn before acting and do not ask
permission for the thing they just described (deleting a piece is the exception,
see section 6). If something is worth saying, say it after, in one line, and
never close by offering to undo what they asked for.

Report three things every time, because they are facts about the result rather
than opinions about it: it will not look like what they asked for (the token
they named resolves to another color), the text came out unreadable on its
background, or the family they named is not in `fontCatalog`. That is not second
guessing them, it is refusing to hand back something broken in silence.

**A color the user names goes in literally.** When they ask for a color their
kit does not have, the hex in that layer is the answer, not a detour through the
brand kit. A literal you chose is still a bug. A literal they asked for is the
instruction, not an exception to apologize for. Once it is applied, one line
offering to put that color in the kit so it holds across every piece. Offer it,
do not do it: `set_brand_kit` repaints every piece in the workspace, so it waits
for their yes. Once, and then let it go.

**A font has a real limit.** `fontCatalog` is the list of families this renderer
can draw, plus whatever the user uploaded into their own kit. A family outside
it exports in a fallback face nobody picked, so here there is no literal worth
writing: name the catalog families closest to what they asked for and let them
choose. That is the renderer talking, not taste.

### Loading a brandbook

The kit does not have to be typed by hand. The user hands you the brandbook (a
PDF, a style guide, the URL of their site) and you are the one who reads it:
pull out the palette, the typefaces and how the brand talks.

1. **Propose it in text first.** The seven colors with their hex and what each
   one is for, the three font slots, and the voice in a few lines. Map every
   family to `fontCatalog` from `get_brand_kit` before you propose it: a family
   this renderer cannot draw exports in a fallback face nobody picked, so name
   the closest catalog families and let the user pick.
2. **Write on their yes**, with `set_brand_kit`. Pass `expectedIdentity` from the
   `identityFingerprint` you read before proposing the change. A
   `BRAND_KIT_CONFLICT` means someone changed the identity: reread, explain the
   affected differences, and resolve them before retrying. Do not blindly
   replace the fingerprint and resend. It is a partial patch, so a
   brandbook that only defines two colors sends two colors and leaves the rest
   alone. An invalid value rejects the whole call and writes nothing.
3. **Keep `previous`.** The result echoes the values you overwrote. That is the
   only undo there is: restoring means calling `set_brand_kit` again with them.

Why the yes matters: pieces store `$brand.*` tokens, so the kit is not a setting
that applies from now on, it repaints everything the user already made.

The logo does not travel this way, it is a file. With a public URL,
`import_image_from_url` brings it into the library. Without one, the user drags
it into Tu cuenta, and that is the whole step.

### Onboarding your user

A new account lands in a welcome wizard: ten steps over nine screens asking
who the brand is, what it looks like, how it talks, what they like (references,
three words for the look, posts they like), what the content should achieve,
which networks to connect, and then a first piece made with you. You can answer part of it, and you should offer to. You have been
talking to this person; the form has not.

`get_onboarding_status` tells you where they are. It comes back with the ten
steps, each with `reached` and `completed`, a Spanish `label`, and
`pendingYouCanFill`: the ones still open that are yours.

Three of them are yours:

| step | tool | what it holds |
| --- | --- | --- |
| `brand_basics` | `set_workspace_profile` | the brand name, what it does, how often it publishes |
| `brand_kit` | `set_brand_kit` | the seven colors and the three font slots |
| `voice` | `set_brand_kit` | how the brand talks |

Both writes are partial patches: send what you know and leave the rest alone.
Writing marks the step answered, and when the user opens the wizard those
screens are already filled in, so they review instead of typing.

The other seven are theirs and you cannot tick them off: `taste` (references
they like, what they never want to see, three words for the look, and posts
they like, which land on the inspirations board you read with
`list_inspirations`), `goal` (what the content should achieve), `networks`
(connecting Instagram or LinkedIn), `agent`
(connecting you, which is done by definition if you are reading this),
`direction` (how to tell the first idea), `first_piece` (they paste a request
into you, you create the piece, the wizard detects it) and `review` (they say
whether that piece represents them; if not, they paste a revision request into
you). `onboarded` only turns true when they walk out the far end themselves.

The rule from the brandbook section holds here in full, and harder: show the
values in text, and write on their yes. A brand you inferred from a conversation
is a guess, and `set_brand_kit` repaints every piece in the workspace.

### Explaining The Agentcy to your person

Sooner or later they ask you to explain, in plain words, what this is and what
they can ask you for (the dashboard's first-steps prompts send them here). Answer
it as a person, not as a tool listing. Do not dump the table from section 3, do
not name ops or JSON fields, and do not read this document out loud. Six or
seven sentences, in their language, is the right size.

What is true, and worth saying:

- **They talk, you design.** There is no editor to learn. They describe the post
  they want the way they would describe it to a designer, and the finished piece
  shows up in their app, already in their brand.
- **The brand is loaded once.** Colors, fonts, logo and voice live in the
  workspace, and every piece you make comes out of them. They do not repeat
  their brand to you on every request.
- **They can see and touch everything.** Nothing you make is hidden in a chat.
  Each piece opens in their app, where they can read it, fix a line of text and
  download or publish it themselves.
- **Changes are a sentence.** "Make the cover shorter", "swap the photo", "same
  post but as a story": they ask, you edit the piece that already exists.
- **What you can reach:** their brand, their images and logos, a stock library,
  every piece and its history, their content calendar, and, if they connected
	Instagram or LinkedIn, publishing and scheduling after the person confirms
	in the app.
- **What you cannot do:** invent a brand they never gave you, publish to a
  network they never connected, or undo something outside their app. Writes are
  immediate and they see them, so anything big gets proposed first.

Good things to hand them at the end, phrased as things they could say right
now: a post about what they do this week, a carousel out of a text they already
wrote, three cover options for one idea. One concrete example beats a feature
list, and starting is the only thing this step is trying to get them to do.

---

## 3. The tools

| tool | what it does |
| --- | --- |
| `get_brand_kit` | colors, fonts, logos, voice, the font catalog |
| `set_brand_kit` | write colors, fonts and voice. A partial patch, and it repaints every piece: propose the values first, write on their yes |
| `get_onboarding_status` | which of the ten welcome-wizard steps the user has answered, and which of them you can answer for them |
| `set_workspace_profile` | write the brand name, what it does and how often it publishes. A partial patch |
| `list_templates` | the templates this workspace saved, each with its `roles` brief. New workspaces start with none |
| `list_pieces` | summaries, newest first, filterable by `status` and `backlog`, paged with `limit` and `cursor` |
| `get_piece` | the row, the full doc and `manualAdjustments` (what the person changed by hand since your last save) |
| `list_calendar` | what is planned between two dates |
| `check_brand` | the deterministic findings on one piece |
| `check_quality` | deterministic mechanical, compositional and anti-slop findings with evidence |
| `compose_piece` | create a piece from narrative, one chosen design direction and typed slide intent |
| `create_piece` | from a doc you design, from a saved template, or blank, in any format |
| `update_piece` | a batch of ops against a baseVersion |
| `repair_quality` | one bounded CAS pass for findings with an unambiguous precomputed fix |
| `complete_design_request` | associate the app requestId with the finished piece after all edits and review |
| `set_piece_meta` | title, category, plannedDate, status, backlog |
| `duplicate_piece` | a copy of a piece, optionally into another language |
| `save_as_template` | keep a piece's shape for next time |
| `list_assets` | the workspace media library: everything already copied in |
| `list_inspirations` | the visual references the user saved. Read before choosing a direction: patterns yes, colors and fonts no |
| `add_inspiration` | save a link or an image URL the user pointed at, with their note |
| `list_backgrounds` / `import_background` | the house background textures, no credit attached. For a texture, look here before `search_stock` |
| `import_image_from_url` | copy an image into the library |
| `search_stock` | free to use photos, with their credit |
| `import_stock` | save one of those photos into the library |
| `render_preview` | a single use URL of the real render |
| `delete_piece` | to the trash, or `purge: true` for good |
| `get_reach_config` / `set_reach_config` | the Instagram follow tool's config (Studio plan): niche accounts, own accounts, never-follow, follow cap. The config ships inside the script the user copies, so tell them to re-copy it after changes. Reach automation and follower collection may violate Meta's terms and may lead to limits, suspension, or account closure; never promise safety or help evade enforcement |

### The flow that works

1. **`get_brand_kit`.** Before writing copy or designing. Include the current
   request and piece when editing; request recent work when continuity matters.
   Refresh context when the user changes the brand or the task.
2. **`list_inspirations`.** What this person already said they like, and how
   those references are built. Section 2 says what travels across and what
   never does.
3. **Choose a direction before coordinates.** For an open brief or a new piece
   with several slides, compare a few concise approaches that explain the
   message differently. Choose against content, brand voice, feed readability
   and real assets. Explain the tradeoff without inventing a weighted beauty
   score. A specific request or a local correction does not need three new
   directions. Preserve approved parts; do not build complete pieces to discard.
4. **`compose_piece`.** Send the narrative, the chosen direction and one typed
   intent per slide. The deterministic compositor selects an explainable recipe
   for each slide using the live brand DNA. Supply `assetId` for one photo or
   ordered `assetIds` for a multi-photo relation; use explicit labels and a
   `visualRelation` when comparing photographs. Every photo must be available
   content in this workspace. References and vetoed photos cannot be used.
   Dimensions alone do not establish that a crop preserves the subject.
   A fitting saved template remains useful, and
   `create_piece({ doc })` remains the escape hatch for a truly custom layout.
5. **`update_piece`.** Make one focused batch when the result needs correction.
   Read both `warnings` and the compact `quality` state returned by every
   mutation. Settle the cover line with care: it decides whether any of the
   rest gets read.
6. **`check_brand`, then `check_quality`.** The first checks kit consistency.
   The second checks broken geometry, hierarchy, composition and explainable
   anti-slop signals. Fix errors. Judge warnings in context; do not sand away a
   deliberate editorial decision just to make the report empty.
7. **One repair pass.** When a finding carries `autoFix`, `repair_quality` can
   apply the safe op with CAS and a receipt. Otherwise make one focused
   `update_piece` batch. Run `check_quality` once more. Interactive work may
   take a second focused round, but never enter an open-ended improve-design
   loop.
8. **`render_preview`.** If you can open a URL, look at it. If you cannot, say
   so and give the URL to the user. Never claim to have seen a piece you did
   not open. `slides: [1, 3]` renders only those slides, 1 based, when you want
   to check one fix instead of pulling the whole carousel.
9. **`set_piece_meta({ plannedDate })`.** Put it on the calendar. When the piece
   is a proposal rather than a commitment, use `set_piece_meta({ backlog: true })`
   instead: a backlog piece is an **idea**, it stays off the calendar and waits
   in the Undated strip of the user's home screen until they give it a date.
   Propose there freely, and leave the promotion to them.

**Write the captions the piece is actually going out on.** Instagram is the
default. When the user mentions that they publish on LinkedIn, or asks for it,
write `captions.linkedin.text` too, in the same batch: it is a second piece of
writing, not the Instagram one pasted over (section 10). Nothing in the tools
reports which networks a workspace connected, so ask or take their word for it
rather than assuming.

Semantic composition is the normal path for new work because it keeps the
model on narrative decisions and the product on geometry. A saved template gets
you a shape that already worked in this workspace. Freehand PieceDoc remains
available when neither grammar represents the content.

---

## 4. Editing: ops and versions

Every edit is `update_piece({ pieceId, baseVersion, ops })`.

**`baseVersion`** is the `version` you read from `get_piece`. The save compares
it against the database inside the same statement, so if the user saved from
the canvas while you were composing, your write is rejected instead of
overwriting them. This is the normal case in this product, not a rare one: the
user in the canvas and their agent over MCP, at the same time.

**The batch is atomic.** Up to 20 ops, applied in order to a copy. If op 7
fails, nothing is saved, including ops 1 to 6. You never have to work out how
far it got.

**One call carries the whole change.** A change that touches five slides is ONE
`update_piece` holding the ops for all five, never one call per slide. Five
round trips is five times the wait for the person watching their canvas, for
exactly the same result.

**Validation is lenient in one direction only.** A bad cosmetic value degrades
to the default and comes back in `warnings` (`fontSize: 9999` becomes 48). A
structural error (unknown layer id, a batch that would leave zero slides)
applies nothing and returns an error with a hint. Always read `warnings`: they
are the diff between what you asked for and what was saved.

### The 14 ops

Where the usual changes live: the color behind a slide is `set_background`, a
layer's color, weight or family is `set_layer_style`, the words are `set_text`.
Each block below is the complete shape of its op, not one variant of it. Copy it
and fill it in. Nothing here needs a probe call to confirm.

**`set_text`** the workhorse. Replaces the content of a text layer.

```json
{ "op": "set_text", "layerId": "ly_a1", "content": "Line one\nLine two",
  "fontSize": 72, "color": "$brand.ink", "align": "center" }
```

`content` is plain text with newlines. Never HTML: it is drawn as text, so
markup will appear literally. `fontSize`, `color` and `align` are optional
conveniences for the fields you most often change alongside the words.

**`set_layer_style`** patches any other field of a layer.

```json
{ "op": "set_layer_style", "layerId": "ly_a1",
  "patch": { "fontWeight": 700, "letterSpacing": -2, "textTransform": "uppercase" } }
```

The patch is validated against the layer's real type: sending `content` to an
image layer is an error, not a silent no-op, because it means you think you
edited something you did not. The patch is partial per field but not inside a
field: sending `frame: { x: 10 }` replaces the whole frame and the rest reverts
to defaults. To nudge a layer, use `move_layer`.

Layers may share an optional `groupId` (1–64 characters) within one slide.
This is a flat selection group in the manual editor, not a parent transform:
frames remain absolute and drawing order is unchanged. Use a single batch of
`set_layer_style` patches to assign the same group ID to related layers; use
`{ "groupId": null }` to remove membership. Preserve existing groups when
editing words or style. Give duplicated groups new IDs within the same slide.

Two more editor flags live on every layer. `hidden: true` keeps the layer in
the document but it never renders or exports; the person toggles it from the
layer stack, so do not delete a hidden layer to "clean up" and do not count it
when judging the visible composition. `locked: true` only means the editor's
canvas will not grab it with the mouse; you may still edit a locked layer with
ops. Both are set with `set_layer_style` and default to visible and unlocked.

**`move_layer`** merges into the frame, which is what you want 90% of the time.

```json
{ "op": "move_layer", "layerId": "ly_a1", "frame": { "y": 520 }, "rotation": -4 }
```

**`reorder_layer`** changes the z order inside its slide.

```json
{ "op": "reorder_layer", "layerId": "ly_photo", "to": "back" }
```

`to` is `"front"`, `"back"` or a 0 based index. An index out of range is
clamped with a warning rather than refused.

**`add_layer`** adds one to a slide. Max 20 per slide.

```json
{ "op": "add_layer", "slideId": "sl_2", "layer": {
    "type": "text", "role": "kicker",
    "frame": { "x": 90, "y": 180, "w": 900, "h": 80 },
    "content": "CASE STUDY", "fontFamily": "$brand.mono", "fontSize": 28,
    "letterSpacing": 6, "textTransform": "uppercase", "color": "$brand.secondary" } }
```

You can omit `id`: one is generated and appears in the `outline` of the result.
The new layer goes on top of the stack.

**`remove_layer`**

```json
{ "op": "remove_layer", "layerId": "ly_a1" }
```

**`set_background`** replaces a slide's background.

```json
{ "op": "set_background", "slideId": "sl_1",
  "background": { "type": "color", "color": "$brand.bg" } }

{ "op": "set_background", "slideId": "sl_1", "background": {
    "type": "gradient",
    "gradient": { "type": "linear", "angle": 160,
      "stops": [ { "color": "$brand.primary", "at": 0 },
                 { "color": "$brand.ink", "at": 1 } ] } } }

{ "op": "set_background", "slideId": "sl_1", "background": {
    "type": "image", "assetId": "<uuid>", "blur": 0,
    "focalPoint": { "x": 0.5, "y": 0.35 },
    "overlay": { "color": "$brand.ink", "opacity": 0.45 } } }
```

The `overlay` is how a light headline stays readable over any photograph.
Gradients are linear only, 2 to 4 stops, `at` from 0 to 1.

**A flat background is never a bare hex.** Add a procedural texture on top of
the brand color: `texture` is `fine`, `coarse`, `paper`, `fabric` or `film`,
and `grain` is its intensity from 0 to 1 (0.35 to 0.6 reads as material, above
0.8 reads as noise). It is drawn by code, so it costs nothing and belongs to
nobody. A flat background without texture looks cheap next to one with it.

```json
{ "op": "set_background", "slideId": "sl_2", "background": {
    "type": "color", "color": "$brand.bg", "texture": "paper", "grain": 0.45 } }
```

**A photo gets a treatment, not just a blur.** Four optional fields sit a
photograph in the piece:

- `grade`: `none`, `editorial` (contrast up, brightness down, a little
  saturation: the default for a photo with text on it) or `immersive` (darker
  and much more saturated: for a cover where the image is the argument). Use
  the same grade on every photo of a piece, including stock and `og:image`
  imports, so they read as one set.
- `grain` 0 to 1 and `vignette` 0 to 1, film grain and darkened edges.
- `overlay.veil`: how the overlay is distributed. `flat` (or absent) is the
  even veil. `low` opens at the top and closes at the bottom, for text that
  sits low; `high` is the reverse; `window` keeps the middle clear for a
  subject; `card` darkens the middle band for a text block there. The veil
  is drawn with ten stops so no seam shows. Use it when there is text on the
  photo and the subject lives in one half; skip it on a photo without text.
- `scrim`: a local radial shadow behind ONE text block, in canvas pixels, so
  the rest of the photo stays clear: `{ "x", "y", "w", "h", "opacity" }`.

```json
{ "op": "set_background", "slideId": "sl_1", "background": {
    "type": "image", "assetId": "<uuid>", "blur": 0,
    "focalPoint": { "x": 0.62, "y": 0.4 },
    "grade": "editorial", "grain": 0.3, "vignette": 0.35,
    "overlay": { "color": "$brand.ink", "opacity": 0.6, "veil": "low" },
    "scrim": { "x": 60, "y": 880, "w": 720, "h": 380, "opacity": 0.5 } } }
```

**`set_image`** points an image layer at an asset.

```json
{ "op": "set_image", "layerId": "ly_photo", "assetId": "<uuid>",
  "focalPoint": { "x": 0.5, "y": 0.3 } }
```

`assetId` must be a real uuid from the workspace media library: `list_assets`
shows what is there, and `import_image_from_url`, `import_stock` or
`import_background` put something new in it. `focalPoint` is the point kept in
view when the frame crops: `{ x: 0.5, y: 0.3 }` keeps a face near the top whole.

**`add_slide`** appends, inserts, or duplicates. Max 20 slides.

```json
{ "op": "add_slide" }
{ "op": "add_slide", "after": "sl_2" }
{ "op": "add_slide", "after": "sl_2", "from": "duplicate:sl_2" }
```

Duplicating is the cheapest way to extend a carousel: the layout comes back with
fresh ids and you only rewrite the text.

**`remove_slide`** fails on the last remaining slide (a piece needs at least
one).

```json
{ "op": "remove_slide", "slideId": "sl_3" }
```

**`reorder_slides`** takes an exact permutation: every current id, once each.

```json
{ "op": "reorder_slides", "order": ["sl_1", "sl_3", "sl_2"] }
```

**`set_captions`** writes the copy per network. Merges per network; `null`
clears one.

```json
{ "op": "set_captions", "captions": {
    "instagram": { "text": "...", "firstComment": "Link in bio" },
    "linkedin": { "text": "..." },
    "x": { "thread": ["First tweet", "Second tweet"],
           "imageMap": ["slide:1", "none"] },
    "tiktok": null } }
```

Limits: Instagram and TikTok 2200 characters, LinkedIn 3000, one tweet 280, a
thread 25 tweets. `imageMap` is positional: which slide rides with each tweet,
or `"none"`.

**`set_format`** moves the whole piece to another canvas and reframes it.

```json
{ "op": "set_format", "format": "9:16" }
```

Every layer of every slide is scaled by ONE factor and centred inside the safe
area of the new format, and a text block that ended up outside is pulled back
in. Ids do not change, so the ops you were about to send still address the right
layers. Sending the format the piece already has does nothing.

A reframe moves a design, it does not redesign it. Taking a 4:5 carousel to 16:9
leaves room on the sides, and taking it to 9:16 leaves room above and below:
that space is yours to fill, and filling it is the difference between a piece
made for the format and one converted to it.

**`set_meta`** title, category and private notes.

```json
{ "op": "set_meta", "title": "Why retention beats reach",
  "category": "playbook", "notes": "Angle came from the user's Q3 numbers" }
```

`notes` is never published. It is the right place to record why a piece is the
way it is, for whoever (you, next week) picks it up again.

### Field ranges worth knowing

Values outside these degrade to the default with a warning, they do not fail.

| field | range | default |
| --- | --- | --- |
| `fontSize` | 8 to 400 | 48 |
| `fontWeight` | 400, 500, 600, 700, 800 | 400 |
| `lineHeight` | 0.8 to 2.5 | 1.2 |
| `letterSpacing` | -5 to 40 | 0 |
| `opacity` | 0 to 1 | 1 |
| `rotation` | -360 to 360 | 0 |
| `hidden` | `true` or `false`; hidden layers stay in the doc but never render | absent (visible) |
| `locked` | `true` or `false`; the editor canvas cannot grab a locked layer | `false` |
| `borderRadius`, `radius` | 0 to 540 | 0 |
| text `content` | 1000 characters, truncated | "" |
| frame `w`, `h` | 1 to 4000 | 1080 x 200 |
| text `scale` | `cover`, `title`, `closer`, `hero`, `intro`, `body`, `label`, `cue` | absent |
| shape `material` | `flat`, `glass` | `flat` |
| shape `bleed` | `left`, `right`, `top`, `bottom` | absent |
| background `texture` / `grain` | 5 textures / 0 to 1 | absent |
| photo `grade` / `vignette` / `overlay.veil` | 3 grades / 0 to 1 / 5 veils | absent |

**`scale` is the type scale by role and format.** When a text layer carries
`scale`, the renderer ignores `fontSize` and uses the size the format was drawn
for. At 1080 x 1350: cover hook 98, title 80, closer 92, hero number 190 with
line height 0.86, intro 34, body 40, label 26, cue 30; display line height
never below 1.12. Other formats scale from the width (9:16 a little larger,
16:9 smaller). Prefer `scale` over guessing a number; deviate only when one
short line owns the slide, and then by writing `fontSize` without `scale`,
+6 to +14 at most.

**`material: "glass"` is the liquid glass recipe**, one recipe for the whole
product: tint from the `fill`, backdrop blur, a top sheen, a 1px rim, radius 24
and a deep shadow. Over a photo the tint is dark; over a flat background it
inverts to a faint light glass, because there is nothing to blur. It is the
material for a live product over a photograph and for one chip or one card
that has to float; it is not a way to make every box look designed. One glass
surface per slide, and only where it earns the place.

**`bleed` says which edge a shape runs off.** The renderer drops
the radius on that side, and the frame has to cross that edge for real: a
bleeding component whose corner is still visible reads as a cut, and
`check_quality` reports it as `visible_bleed_corner`. Push the content inside
the component about 108px away from the bleeding edge.

Shapes are `rect`, `pill`, `ellipse` or `line`, with a `fill` (a color ref or a
gradient) and an optional `stroke`. Logos take a `variant`
(`primary`, `icon`, `wordmark`) and a `tint` (`"original"` or a color ref, for
placing a logo over a dark photo). A logo has no `assetId`: it resolves against
the brand kit, which is why a doc or a saved template can ship with the logo
already placed.

---

## 5. Images: nothing renders a URL

A piece can only draw an image from the workspace library, by its `assetId`. No
layer takes a URL: an external one goes dead and the piece loses its photo.
**Look before you import**: `list_assets` is cheap, the user usually has their
own photos in there, and a second copy is clutter they have to clean up.

```
list_assets                     what the workspace already has
  -> list_backgrounds           the house textures, then import_background
  -> import_image_from_url      a specific image you have a URL for
  -> search_stock               find candidates, free to use commercially
     -> import_stock            save the one you picked
        -> set_image / background   place it in the piece
```

**`import_image_from_url({ url, kind })`** stores the file: jpeg, png, webp and
svg, up to 20 MB. Internal and private addresses are refused on every redirect,
because you read that URL somewhere. `kind: "logo"` lands it in the logo picker.

**`list_backgrounds` / `import_background({ slug })`** are the house collection:
neutral textures made for this product, with no credit to carry. They are files,
not assets, so they do not appear in `list_assets` until you copy one in. A
`textura` is opaque and covers the frame, a `trama` is nearly transparent and
needs a color background under it. The same slug twice returns the same asset.

**For a background texture, the house collection comes before stock.** Going to
a photo bank for a texture puts a stranger's photo, credit and all, on a
piece where the user had the texture of their own brand one call away.

**`search_stock({ query, page })`** searches the photo banks (Pexels, then
Unsplash, then Openverse; the first with results answers, and a bank that is
down is skipped) for photos free to use commercially and to modify. Nothing is
saved yet: the results are candidates with a preview, a `bank` and a credit.
Search in the subject's language, not the user's: the banks index English
metadata, so `boxing gym` beats `gimnasio de boxeo`.

**`import_stock({ id })`** saves one of those into the library. Send the id and
nothing else: the file URL and the credit come from the bank, which keeps a
photo from being stored under the wrong author. The credit comes back in
`list_assets` for the piece to carry. Import the one you chose, not three.

**A stock photo is the last resort, not the first.** A generic image of a
stranger in an office says nothing and looks like every other post in the feed.
The user's own photo, a texture, a shape or type on a color field all beat
it. Never invent what a photo shows: place it, then look at it.

**Unless the person asked for one.** Feedback that names photos or a photo
background ("quiero fotos", "no tiene foto de fondo", "no solid backgrounds")
outranks the rule above: the revision leaves with a photo on it, or with a
house texture and one line saying why. When `search_stock` answers
`UPSTREAM_ERROR` or `RATE_LIMITED`, do not loop on it: fall back to
`list_backgrounds` and `import_background`, or ask for one concrete photo and
place it with `import_image_from_url`. Never close a request with a cosmetic
change (nudging text, resizing a layer) and report it as done; say what
blocked you instead. The person sees every version, and a "new version" that
looks identical costs more trust than an honest "the photo bank was down".

### Designing with few resources

Most workspaces have one usable photo or none. That is a composition problem
with known answers, not a reason to stop or to fill the gap with stock:

- **No photo**: type carries the piece. A strong hook on `$brand.bg`, one
  number or one line as the visual, shapes and `$brand.bgAlt` blocks to build
  rhythm, the logo small. Choose a `type` or `data` dominant mode; do not
  pretend an image exists.
- **One photo**: it is the cover, cropped with `fit: "cover"` and a focal
  point, or a full-bleed background with a veil under the text. The inner
  slides go typographic; reusing the same photo on every slide reads as a
  template.
- **A product cutout or a logo only**: give it air. A cutout on a flat color
  with one line beats a fake scene around it.
- **Inspirations are not material**: `kind: "inspiration"` assets never go
  into a piece; they tell you what the person likes.

When one concrete photo would change the piece, ask for exactly that one and
say how to take it ("una foto del local con la persiana abierta, de día,
horizontal"), then design without it in the meantime. Do not ask for "some
photos".

---

## 6. Beyond one piece

**`duplicate_piece({ pieceId, language })`** copies the design into another
language: same layout, same photos, fresh ids, title suffixed. The copy comes
back with the ORIGINAL words still in it, because translating is your job and
not the server's. Read the outline it returns, then rewrite the text layers and
the captions. Leave the layout alone, it already fits.

When you use it to offer variants of the same piece, each round changes ONE
thing: the hook, or the photo, or the background mode, never two at once. The
user decides by looking, and two variants that differ in three ways cannot be
compared. Freeze the structure, change one dimension, show both.

### Coherence across pieces

Pass `includeRecent: true` to `get_brand_kit` before a new piece: the summaries
show what the brand already said and how (hooks, categories, planned dates).
A series repeats a structure on purpose and says so in the title or category;
a launch may ask for a different direction. What you avoid is the accident:
the same hook shape three times in a row, the same photo treatment on every
piece, or a piece that contradicts last week's. When several pieces are asked
together, plan them as a set (one angle each, a shared thread) and compose
them one by one. Read a recent piece with `get_piece` when you need its actual
composition; the summaries are not render inspection.

**`save_as_template({ pieceId, name, description })`** keeps the shape of a
piece that came out well, so later pieces can start from it with
`create_piece({ templateSlug })`. The words are kept too: a template of your own
is `I want to make this piece again`, not a blank layout.

**`check_brand({ pieceId })`** runs the deterministic check: a color or a font
that is not in the kit, a text block that does not fit its frame, type below the
readable floor, an empty layer, an image pointing at an asset that no longer
exists, text the exact same color as whatever sits behind it (sameness, not
contrast: dark grey on black passes). `error` will render wrong for sure;
`warning` is often a decision somebody made on purpose, so a literal the user
asked for lands here and stays exactly as it is. It says nothing about whether
the piece is good, only whether it is consistent: the last pass before you hand
a piece over, never an approval.

**`check_quality({ pieceId, displayWidthPx? })`** is a separate deterministic
pass for design quality. `displayWidthPx` is the intended displayed width in
CSS pixels, not the export resolution; omitting it explicitly assumes 360px.
The report returns stable codes and reproducible evidence. Read `assessment`
coverage with its status: `pass` means no selected rule found a problem, not
that the render, brand fit, editorial usefulness or taste has been approved.

Text wrapping and box intersections are estimates and return warnings. Inspect
the shared render with fonts loaded before shortening copy or moving a layer.
Contrast is calculated only for known uniform colors, including alpha and cue
opacity, against the 4.5:1 normal / 3:1 large-text reference at delivered size.
Photo regions, glass, gradients, partial or later overlays and unresolved tokens
are listed as unmeasured. No browser glyph metrics or pixels are inspected by
this pass. A missing measurement is never a contrast pass.

Warnings are review signals and never block a human save or export. The mode
remains `observe`: repair an observed cause once when the fix is clear, and keep
a deliberate decision when it serves the argument.

The stable codes are grouped by subject, not by measurement certainty:

- Mechanical: `layer_out_of_bounds`, `text_overflow`, `critical_overlap`,
  `unreadable_contrast`, `illegible_text_size`, `missing_asset`.
- Composition and coherence: `broken_proximity`, `weak_dominant`, `dead_space`,
  `top_stack`, `headline_orphan`, `alignment_drift`, `accent_mass`,
  `surface_overload`, `material_drift`, `background_mode_drift`.
- Anti-slop and specificity: `orphan_divider`, `false_button`,
  `decorative_container`, `repeated_chrome`, `template_rhythm`,
  `missing_content_visual`, `grammatical_triplet`, `generic_closer`,
  `pseudo_quote`, `decoration_without_job`, `cue_outside_cover`,
  `generic_cue`, `visible_bleed_corner`.

**`complete_design_request({ requestId, pieceId, baseVersion })`** returns work
to a waiting screen in the app. Use the requestId supplied by the user's copied
request; finish all edits, render inspection and focused repairs first. The tool
writes a completion receipt through CAS and does not approve or publish. The
server captures that delivery and its identity separately, so later edits or
another request cannot replace it. A prepared request may name a specific piece;
finish that piece rather than silently substituting another.
On conflict, read the current document and review it before retrying. Replaying a
completed receipt is idempotent; `completedVersion` is the version delivered,
while `version` may include later changes. Do not complete a different request
because its piece happened to be the newest. For ordinary work without a requestId,
continue to return the pieceId normally.

**`repair_quality({ pieceId, baseVersion, codes, maxFixes })`** only acts on a
finding that already carries a safe `autoFix`: remove an orphan divider, move a
layer that is only slightly outside bounds, or reduce one isolated oversized
accent shape. It uses CAS, writes normal version history and analyzes the saved
result again. It never rewrites copy or makes a taste decision. Call it once in
normal generation; if it says no safe repair exists, use the suggested fix or
keep the deliberate warning.

**`delete_piece({ pieceId })`** sends the piece to the trash, where the user can
restore it from Tu cuenta. `purge: true` deletes it for real, version history
included. Ask before either one, every time, even when they told you to clean
up: they cannot see which piece your description resolved to.

---

## 7. Errors, and what to do about each

Every error is `{ "error": CODE, "message": ..., "hint": ... }`. The `hint` is
written for you and usually contains the exact data you need to retry, such as
the list of layer ids that do exist.

| code | meaning | recovery |
| --- | --- | --- |
| `VERSION_CONFLICT` | someone saved while you worked, almost always the user watching their canvas | `get_piece`, look at what changed, rebase your ops and resend. Never resend the same `baseVersion`, and if their change conflicts with what you were about to do, say so instead of quietly winning |
| `LAYER_NOT_FOUND` | no such layer id | the hint lists every layer of the doc. Pick the right one |
| `SLIDE_NOT_FOUND` | no such slide id | the hint lists the slide ids |
| `INVALID_OP` | an op is malformed, or aimed at the wrong layer type | the hint says which fields that layer type accepts |
| `LIMIT_EXCEEDED` | 20 slides, 20 layers per slide, or 20 ops | split the batch, or remove before you add |
| `LAST_SLIDE` | you tried to delete the only slide | add one first, or delete the piece |
| `NOT_FOUND` | no such piece in this workspace | `list_pieces`. A piece in someone else's workspace answers identically |
| `INVALID_DOC` | a doc that cannot be recovered | usually zero slides or a missing `schemaVersion` |
| `TEMPLATE_NOT_FOUND` | bad slug | `list_templates` for the slugs this workspace saved |
| `EMPTY_PATCH` | `set_piece_meta` with nothing in it | send at least one field |
| `INVALID_IMAGE` | the URL did not answer with an image we accept | jpeg, png, webp or svg, public address. Do not retry the same URL |
| `FILE_TOO_LARGE` | over 20 MB | find a smaller version |
| `INVALID_QUERY` | empty stock query, or a page past 12 | narrow the search |
| `INVALID_RANGE` | `list_calendar` with `from` after `to` | send the earlier date as `from` |
| `PRO_REQUIRED` | a Studio-only design or Reach operation on a Basic workspace | solve the slide with type, shapes or a photo, or explain what Studio unlocks. Do not retry |
| `SERVER_NOT_CONFIGURED` | previews are not wired up in this deployment | nothing you can fix. Every other tool still works |
| `PREVIEW_FAILED` | the preview permit could not be issued | retry once |
| `STORAGE_ERROR` | a house background file could not be read | retry once, then use something else |
| `RATE_LIMITED` | the photo bank is throttling THIS SERVER | not the user's fault. Wait a minute, or work with `list_assets` |
| `UPSTREAM_ERROR` | the photo bank is down | retry once, then say so |
| `COMPOSITION_UNRESOLVED` | one slide of `compose_piece` fits no layout, almost always a `claim` or `support` longer than any family holds | the message names the slide, the field and the budget. Rewrite that field shorter and call again. Nothing was created; retrying unchanged returns the same error |
| `DB_ERROR`, `TOOL_FAILED` | the server had a problem | retry once, then stop and tell the user |
| `UNAUTHORIZED` | the authorization is invalid or expired | reconnect The Agentcy from the client and authorize the workspace again |

---

## 8. Design rules

These are the difference between a piece that validates and a piece that ships.

**Contrast is not optional.** Text over a photo needs either
`$brand.inkInverse` plus an overlay on the background, or a shape behind it.
`check_brand` catches only the exact same color: one shade apart already passes.

**Respect the character budgets.** `maxChars` on a role is the number the layout
was drawn for. Going 20% over usually works; going double overflows the frame
and there is no reflow to save you.

**One idea per slide.** A carousel is read at speed. If a slide needs two
headlines it needs to be two slides.

**One dominant, not one template.** Each slide needs a clear first read, but the
carousel must change structure when the job changes. A hook, a comparison, a
process and a conclusion should not be the same headline-body-footer stack with
different words. Continuity comes from a controlled device such as an index,
crop, sequence or motif, not identical geometry.

**Every visible object needs a job.** A line separates, connects or measures
progress. A box groups, compares, protects legibility or represents a real
object. A pill is an action only when there is an action. If the explanation is
"it makes the slide feel designed", remove it. Prefer better scale, grouping
and distribution before adding a card, chip, shadow, gradient or icon.

**Accent is punctuation.** Keep the brand accent below roughly ten percent of
the slide unless it is the content itself. A large antagonistic color panel
behind one sentence usually creates a false component and overwhelms the brand
rather than strengthening it.

**Use the whole canvas intentionally.** Do not stack kicker, headline, body and
footer in the top third and call the empty lower half minimalism. Sparse layouts
still need a deliberate center of gravity. If a large region is empty, it must
improve tension, sequence or focus.

**Photo or flat is decided by how much reading the piece asks for.** A photo
background when there is little text or the design is the protagonist; a flat
background when the piece is a deep read. Then look at the feed: call
`list_pieces` before you choose, and alternate flat and photo between
consecutive pieces. Never three flat pieces in a row, never three photos.

**Represent relationships instead of decorating text.** Comparison asks for an
axis or two explicit sides. Sequence asks for ordered steps. Quantity asks for a
visible magnitude. Cause and effect asks for a connection. Three paragraphs in
three identical boxes are still three paragraphs.

**Avoid synthetic wisdom.** Do not turn an unsupported sentence into a giant
quote, repeat three grammatical clones for rhythm, or close with a generic
"save this post" panel. Attribute real quotes. End with a concrete synthesis,
decision or next action earned by the piece.

**Eyebrow is context, never information.** Cover the eyebrow with your thumb:
if the slide loses information, the words are in the wrong slot. When in doubt,
no eyebrow. And a description never continues the sentence of the headline:
the headline is understood alone, the description is a separate explanation.

**Line breaks are measured, and widows are never allowed.** If one line of a
headline ends up much shorter than the other, find another break even if it
splits a phrase: the reader notices the step before the grammar. Write the
breaks yourself with `\n`. A single word left alone on the last line is an
error in every piece, for every brand, and `check_quality` reports it as
`headline_orphan` with error severity, not as a warning to keep.

**The accent has a short list of homes.** One accented word or number per
slide, a hairline, a check mark. Nothing else gets
the accent color. And every figure has ONE home in the piece: never a wall of
stacked numbers, and always tied to what it means for the reader.

**A pill earns its place only with a fact that is nowhere else.** `FREE ENTRY`
yes; `JOIN US` or `DON'T MISS IT` no, because publishing already is the
invitation. Chips or a component on the cover, never both.

**A paragraph in display type is still a paragraph.** A short, large title plus
the fact that completes it in the mono slot below; never four lines of display
as an explanation. This is judgement, not law: sometimes the long line owns the
slide, and you will know because nothing else on it competes.

**Do not invent facts.** Numbers, claims and names come from the user or from
the piece you are editing. If you need a statistic, ask for it. A confident
invented number is the worst thing you can put in someone else's brand.

**Do not author dead fields.** Write only the fields the renderer honors: the
ones in this document and in the doc you read back from `get_piece`. Anything
else is dropped with a warning, so inventing `shadow`, `padding` or `zIndex`
does not fail loudly, it does nothing while you believe you styled something.
Build the effect out of what exists (a shape behind the text) or leave it.

---

## 9. Writing covers that stop the scroll

The cover is the whole piece as far as the feed is concerned. Everything else
you wrote only gets read if the first line earns it.

**The hook is a promise the post has to keep.** A cover that promises more than
the slides deliver is worse than a boring one: it buys a swipe and spends the
reader's trust to do it. Write the hook after you know what the piece actually
proves, or write it first and then make the piece good enough to deserve it.

**Eight words or fewer on the cover.** A hook that needs a second line to work
is not a hook yet, it is a summary. Cut words until taking one more out would
break the sentence.

**The screenshot test.** Read line 1 alone, as if you had seen it in a
screenshot with no brand, no image and no context. If it reads like generic
marketing (`Grow your business with these tips`), it is not a hook. Rewrite it
until it could only have been written about this specific thing.

**Skip or stay.** Every cover falls into one of these columns, and the reader
decides in about a second.

| skip | stay |
| --- | --- |
| literal | emotional |
| generic | specific |
| functional | intriguing |
| about the product | about the reader |

Five moves get a line from the left column to the right one.

1. **Desire over literal.** Not what the thing is, what it gets them.
   `Meal planning app` becomes `Never ask what's for dinner again`.
2. **Intrigue over instruction.** An open loop beats a command.
   `Follow these 5 steps` becomes `The step everyone skips`.
3. **Specific over generic.** A number, a name, a date, a real detail.
   `We grew fast` becomes `From 12 clients to 60 in one winter`.
4. **Reader identity over product.** Put them in the sentence.
   `Our new plan` becomes `For the coach with a waiting list`.
5. **Transformation over static.** Show the before and the after.
   `Better bookkeeping` becomes `Sunday nights back`.

**Choose the cover photo for what it has, not for how pretty it is.** Without
an object or a person in frame, the lower half is texture and the headline
floats. A photo with strong color earns more at the end of the piece than at
the start. Never crop a face or a head, and check every crop where one appears.

**The swipe cue is its own element, and it goes only on the cover.** A text
layer with `role: "cue"` (or `scale: "cue"`), sentence case, in the body
family, centered under the sub, dimmed, with the arrow drawn by the renderer.
It carries a concrete promise: `Deslizá para ver las 3 señales`, never
`Deslizá para ver`. Use it only when the piece is chained storytelling and
there is something to pay for the swipe; an announcement, a quote or a
one-idea piece does not get one. Never by default. `check_quality` reports
`cue_outside_cover` and `generic_cue`.

**A photo has a memory too.** `list_assets` marks a photo the user vetoed
(`vetoed: true` with their reason): never place it, and do not argue with the
reason. Before choosing a cover photo, look at what the recent pieces used:
a cover photo is never repeated, and no photo comes back within 21 days.

**Do not reuse a hook within 30 days.** The feed remembers even when the writer
does not, and the same opening twice in a month reads as a template. Call
`list_pieces` and look at what has already gone out before you settle on a line.

---

## 10. The story arc

A carousel is one argument told in swipes, not a slide deck of related thoughts.

**The cover has to make sense alone.** Most people will only ever see it. If it
only works once you have read slide 2, it does not work.

**Slide 2 is never just the punchline.** If the answer to the cover fits on
slide 2, the piece is a single image, not a carousel. Slide 2 is where the
promise gets sharpened: the stake, the cost, the thing they already suspected.

**Each slide earns the next swipe.** One idea per slide, and the last line of a
slide should leave something open. A slide that closes its own loop completely
is where the reader stops.

**When a middle slide does not close, it is usually competing with the cover.**
Stop fixing its copy and ask whether it repeats the cover's mold instead of
contrasting with it. And repeat chrome only while it orients: what identifies
(the brand footer) goes on every slide; what informs (a date, a venue) is not
repeated once the reader is already placed.

**One slide per piece is built to be saved**, but only when the piece teaches
or explains something: a framework, a checklist, a line sharp enough to keep.
Make it visibly different from the rest. A feature announcement does not need
one. And run the screenshot test on every interior slide, not only the cover:
any slide, alone and without the caption, has to close.

**Read the piece as the most skeptical expert in the user's audience** before
you hand it over. Punish condescension (explaining to a pro what they already
know), the generic SaaS pitch, accusing the reader, and the safest take in the
category. This is what separates a correct piece from a good one.

**Read the slides out loud, in order, as the last thing you do.** If you
stumble, or if two slides could swap places without anyone noticing, the arc is
not there yet and no amount of layout will fix it.

### Recipes by intent

What defines a piece is its intent, not its topic, and each intent brings an
arc and a visual base. These are generic starting points; every brand grows its
own from them, and the ones your user keeps asking for belong in your memory
for that person, next to the words they told you never to use.

- **Opine.** 4 to 6 slides, photo background, minimum text, no CTA: the idea is
  the close. The cover is the take, slide 2 is the cost of ignoring it.
- **Teach.** 6 to 8 slides, flat background, and 3 to 5 middle slides share the
  SAME internal template, because repetition reads as authority. Closes with a
  slide worth saving.
- **Show a product.** 5 slides. Slide 1 hooks, slide 2 is the problem, the
  product appears on slide 3 at the earliest, as a photo or screenshot from
  the workspace library, never described in prose.
- **Prove it with a case.** 6 slides, flat background with small framed photos,
  one measurable signal per slide, and a final slide with no CTA: the result is
  the argument.
- **Sell.** 5 to 7 slides, exactly one slide of pain, then the value dominates.
  One CTA, on the last slide, and nowhere else.

### The caption is the second post

The caption adds a layer the carousel did not give; it never repeats the
slides. Keep it under 150 words, with a single CTA, and not every piece closes
with an ask: an opinion or a case closes on the idea. Write it with
`set_captions` after the slides, in the brand's voice, and read it as if it
were the only thing the reader sees, because on the feed it often is.

**LinkedIn is its own text, not a copy of the Instagram one.** Up to 3000
characters, and the first line is the hook: LinkedIn folds everything after it
behind "see more", so a caption that opens with context is a caption nobody
expands. Write it flat and professional, without chained hashtags and without
emoji, and close on a question or one concrete ask. Write plain text: the
server escapes the reserved characters of LinkedIn's little text format before
posting, so parentheses and dashes in your copy are safe.

Format matters to what you write there. One slide goes out as an image; two or
more go out as a PDF, one page per slide, which LinkedIn shows as a document
with arrows and a page counter. Nobody scrolls a document as fast as a
carousel, so each slide has to read alone.

---

## 11. A full session

The user says: "make a carousel about why most gyms lose members in month two,
schedule it for next Tuesday."

```
1. get_brand_kit()
   -> colors, fonts, voice: "Direct, no hype. Talks about work done,
      not promises. Never uses exclamation marks."

2. Think through three compact directions, not three finished carousels:
   A. type-led argument, sparse, one changing vertical axis;
   B. evidence-led, data dominant, claim paired with one proof per slide;
   C. process-led, ordered sequence, a real progression instead of cards.
   Choose B: it makes the churn evidence the visual dominant and matches the
   direct voice. The signature move is one concrete signal per slide.

3. compose_piece({
     title: "Why members leave in month two", format: "4:5",
     narrative: "argument",
     direction: { thesis: "Retention fails in the gap between motivation and habit",
       dominantMode: "data", density: "balanced", variance: "distinct",
       materialMode: "flat", continuityDevice: "sequence",
       signatureMove: "one measurable signal per slide",
       avoid: ["ornamental dividers", "fake buttons", "repeated cards"] },
     slides: [
       { role: "hook", claim: "Most members leave in month two",
         emphasis: "claim", transition: "open" },
       { role: "evidence", claim: "The cliff starts before habit does",
         proof: "WEEK 6", support: "Week one is motivation. Week six has to be routine.",
         visualRelation: "quantity", emphasis: "proof", transition: "deepen" },
       { role: "mechanism", claim: "The gap has three moments",
         labels: ["First visit", "First missed week", "No return"],
         visualRelation: "sequence", emphasis: "visual", transition: "deepen" },
       { role: "synthesis", claim: "Retention is built before the absence",
         support: "Design the second month, not only the first visit.",
         emphasis: "claim", transition: "close" } ] })
   -> { pieceId: "9f...", version: 1,
        recipes: [ { recipeId: "data_dominant", reason: "..." }, ... ],
        quality: { mode: "observe", status: "warnings", ... }, outline: [...] }

4. check_brand({ pieceId: "9f..." })
   -> { counts: { errors: 0, warnings: 0 }, findings: [] }

5. check_quality({ pieceId: "9f..." })
   -> { status: "warnings", findings: [
        { code: "headline_orphan", slideId: "sl_...", layerIds: ["ly_..."],
          evidence: { estimatedLines: 3, wordCount: 7 },
          suggestedFix: "Adjust width, size or copy to balance the lines." } ] }

6. update_piece({ pieceId: "9f...", baseVersion: 1, ops: [
     { op: "set_text", layerId: "ly_...",
       content: "Retention starts before the absence" } ] })
   -> { version: 2, warnings: [], quality: { mode: "observe", ... } }
   Then check_quality once more. Do not keep changing deliberate warnings.

7. render_preview({ pieceId: "9f..." })
   -> { previewUrl: "https://theagentcy.app/export/9f...?t=...&scale=0.5",
        expiresAt: "...", note: "HTML page, not an image" }

8. set_piece_meta({ pieceId: "9f...", plannedDate: "2026-08-25",
                    category: "playbook", status: "approved" })
   -> { pieceId: "9f...", status: "approved", plannedDate: "2026-08-25" }
```

Then tell the user what you built, in one short paragraph: the angle you took,
how many slides, when it is scheduled, and the preview link. Not a slide by
slide recap, which they can see by opening it.

---

## 12. Connecting

The server speaks Streamable HTTP at `https://theagentcy.app/api/mcp`. New
connections use OAuth 2.1: the client opens the authorization screen, the user
chooses the workspace, and the client sends the resulting bearer token. No new
API keys are created or exposed in the product.

Legacy `ag_...` keys may still authenticate existing integrations. They remain
scoped to one workspace, and there is no tool that takes a workspace as an
argument, because there is nothing else the agent is allowed to see.
