--- name: theagentcy description: Use when designing, editing, checking or scheduling social media pieces (carousels, posts, stories) through The Agentcy MCP server, or when the user mentions The Agentcy, their brand kit or their content calendar. Covers the PieceDoc schema, brand tokens, edit ops, the tool order and the rule that publishing is always confirmed by the person in the app. --- # 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. ### Writing the copy: sound like the brand, not like an AI The number one complaint about AI content tools is that everything sounds the same. The same craft rules apply to you and to the app's own assistant; their source is The Agentcy's internal guide (`POST.md`, section "Copy de piezas"). - Pick one angle before writing: the one thing this piece says that another business in the same trade would not. - The headline states the fact. The supporting line adds what is missing (time, place, detail, who it is for) and never restates the headline. "Desde el sábado" says nothing; "Desde el sábado, medialunas a las 7" does. - Concrete nouns before adjectives: "medialunas de manteca a las 7", not "lo mejor para arrancar el día". - With a photo, the text accompanies it: it does not describe or explain it. - Composition notes ("a full-bleed photo of...", "close-up", "carousel", "slide", "typography") are instructions for you, never words on the piece. - The copy never talks about itself: not about the photo ("con esta foto", "la foto de hoy") nor about the post ("una publicación simple"). What the user tells you ("hacé algo lindo", "no pongas precio") is an instruction, not copy: if they ask to leave something out, leave it out without announcing it is missing. - Headline and supporting line are two complete sentences; the supporting line never finishes the headline's sentence. If an idea does not fit, shorten it instead of splitting it. - Write the way the brand's own texts write: `creativeProfile.voiceSamples` (real posts, the bio, customer messages) first, then `voice`. Same sentence length, vocabulary and punctuation, and the same treatment (vos or tú). Imitate the form, never copy their content or facts: a price or a product in a sample is not evidence for this piece. Pick the samples closest in register and length (a caption for a caption, a headline for a headline), never the ones about the same topic. The user's literal words are the best sample of their voice: start the headline or the caption from their own sentence ("desde el sábado abrimos a las 7"), fixing spelling only. If the brand's texts are funny, dry or blunt, the piece is too; do not flatten it. - A vague request ("algo lindo para esta semana") is never copy. Pick a concrete angle from what the kit and the confirmed knowledge name, or ask. - The caption adds one thing the piece does not say; it never opens by repeating the headline. No hashtags, or at most three specific ones, or the ones the brand already uses. Never a wall of generic tags. - A call to action names its object or does not exist: "Reservá por WhatsApp hasta el viernes", not "Conocé más" or a bare "Te esperamos". - No em dashes, emojis or Markdown in the piece unless the brand's own texts use them. House anti-AI list for Spanish copy. Avoid these unless the brand's real texts use them: "no es X, es Y", "no es X, sino Y", "no se trata de", "más que un", "no sólo X, sino Y"; closers like "también es barrio", "también cuenta", "tiene lo suyo", "y eso se nota", "así de simple"; brochure verbs (potenciá, transformá, elevá, descubrí, sumergite, impulsá, desbloqueá, "llevá X al siguiente nivel"); empty adjectives (único, increíble, imperdible, de calidad, experiencia única, ideal para, pensado para, en un solo lugar); worn openings ("¿Sabías que...?", "En un mundo donde...", "Imaginá..."); and parallel triads of adjectives. `check_quality` reports these as `copy_*` warnings (below); fix them by rewriting with the concrete fact, in the brand's words. ### 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, the voice in a few lines, and 3 to 5 real texts the brand already wrote (a caption, the bio, a paragraph of their site, a message to customers), verbatim, as `voiceSamples`. 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. **Voice samples are evidence, not a description.** Five real texts beat any adjective list: the app's assistant and the copy checks imitate them, and an expression that appears in them (even one on the anti-AI list) is the brand's own and is never flagged. Save them with `set_brand_kit({ voiceSamples })`: the complete list, up to 5 texts of up to 600 characters, copied exactly. Never invent or paraphrase a sample; with none at hand, ask the user to paste two or three posts they liked writing. The rest of `creativeProfile` is never touched by that write. 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 reaches six activation screens: brand basics, identity, objective, agent connection, the first design request and its review. Direction lives in the first request. Voice, taste, inspiration and social connections are optional setup and do not block creating the first design or finishing activation. `get_onboarding_status` preserves the historical step inventory and marks, while `phase`, `requiredForActivation` and `visibleActivationScreen` describe the current flow. `activationSteps` and `optionalSetup` are separate. `pendingYouCanFill` only names unfinished activation data you can provide; `optionalYouCanFill` names optional improvements that can wait. | step | tool | what it holds | | --- | --- | --- | | `brand_basics` | `set_workspace_profile` | brand name and what it does | | `brand_kit` | `set_brand_kit` | colors and font slots | | `voice` (optional) | `set_brand_kit` | how the brand talks | Both writes are partial patches: send what you know and leave the rest alone. Writing records the corresponding step. Do not infer that an unmarked optional step prevents work, and do not invent a preference to complete its checkbox. The person chooses their objective and connections, reviews the result and finishes activation in the app. Use `complete_design_request` to return a request-linked result after edits and review; a saved piece alone is not its completion receipt. `onboarded` remains the app's recorded completion state. 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, voice and `voiceSamples` (3 to 5 real brand texts). A partial patch, and it repaints every piece: propose the values first, write on their yes | | `get_onboarding_status` | activation progress and optional setup, preserving historical step marks | | `set_workspace_profile` | write the brand name and what it does. 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 | | `get_week_plan` | the week's 3 to 5 ideas and upcoming commercial dates, proposed by code (no model): prompt, brief, structure and open questions per idea | | `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 | | `create_variant` | "otra versión": the same content in another layout family, as a new version of the same piece. No rewriting | | `adapt_piece` | the same content in another format (story, square...) as a new linked piece; never truncates copy | | `create_review_link` | a read-only approval link for one piece that the user can send to a partner or client | | `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 HTML URL of the real render | | `render_preview_image` | actual rendered image blocks, individual slides or a contact sheet, with version and resource receipt | | `get_photo_analysis` / `set_photo_analysis` | inspect source photo pixels and read/write explicit versioned subject/crop observations | | `get_publication_results` | stored publication outcomes and timestamped metrics; read only, with missing values preserved | | `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_image`.** Inspect the returned image blocks. Request up to four 1-based slide numbers, or a `contact_sheet`; with no selection the first four are returned. Read `pieceVersion`, `identityFingerprint` and `hasMore`. Resource readiness does not establish aesthetic approval. `render_preview` remains the HTML/browser fallback. If image rendering or inspection is unavailable, say so; never claim to have seen a design you did not inspect. 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": "", "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": "", "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": "", "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 2200 characters, TikTok photo descriptions 4000, 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. **`create_variant({ pieceId, baseVersion, variant? })`** recomposes the same content with another eligible layout family, without touching a word, and saves it as a new version of the same piece (the previous layout is one restore away). It works on pieces whose intent was recorded: everything `compose_piece` makes and everything the mobile assistant makes. **`adapt_piece({ pieceId, format })`** makes the story, square or landscape version as a new piece linked to the source; if a text does not fit the new canvas it creates nothing and tells you which one, so you shorten it with the user instead of the server truncating it. **`get_week_plan()`** answers "¿qué publico esta semana?" without a model: ideas built from the objective, confirmed knowledge, pillars, the commercial calendar of the country and what was already published, plus the dates coming up. Show the ideas, ask the open `questions` of the one the user picks, and design only what they approve. **`create_review_link({ pieceId })`** gives the user a link to send for approval; create it only when they ask, and hand them the URL. 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. **`render_preview_image({ pieceId, expectedVersion?, slides?, layout? })`** returns a JSON receipt followed by actual MCP image blocks from the existing export worker. `layout` is `individual` (default) or `contact_sheet`; each call renders at most four slides. The strict resource receipt requires the complete manifest and successful loading of its declared fonts and images. An older worker returns `PREVIEW_UNVERIFIED` instead of claiming this was checked. A version/identity conflict requires rereading the document and kit. This tool never publishes, consumes a download or creates a release. **Photo observations.** `get_photo_analysis({ assetId, includeImage? })` returns the source image by default, together with `version`, `assetFingerprint`, `sourceDimensions` and existing metadata. Inspect the pixels. Coordinates use `coordinateSpace: "displayed_image"`: the complete source, auto-oriented as the browser displays it, before any crop. `set_photo_analysis` takes that version as `baseVersion`, the unchanged `expectedAssetFingerprint`, and `analysis` with `schemaVersion: 1`, the returned dimensions, `analysisComplete`, normalized `protectedRegions`, optional `focalPoint`/`cropSafe`, and `provenance`. Each region has `{x,y,w,h,kind}` with kind face, hand, subject, text or logo. `agent_visual` provenance names the agent, ISO `observedAt` and concrete visual `evidence`; `reviewed_override` additionally records the user's actual `userConfirmation`. Never invent a confirmation. An empty region list means you inspected an empty scene; dimensions alone are not an analysis. Partial, stale, revoked or unavailable metadata retains the safe fallback. Send `analysis: null` with CAS to revoke an observation. The server never calls an image-analysis model. Saved current observations feed `compose_piece` without changing existing PieceDocs. **`get_publication_results({ pieceId?, postId?, limit? })`** reads already stored outcomes. It never refreshes a provider or publishes. Compare snapshots at similar `ageHours` and account context; null metrics are unknown, not zero. Engagement is not aesthetic approval or permission to change durable taste. **`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`. - Copy (the same rules the app's assistant enforces, read against the brand's own texts in `voice`, its language and its treatment): `copy_art_direction`, `copy_repeated_sentence`, `copy_repeated_phrase`, `copy_support_repeats_headline`, `copy_ai_phrase`, `copy_brand_repeated`, `copy_dash_emoji_markdown`, `copy_empty_cta`, `copy_address_mismatch`, `copy_filler_hashtags`. They cover slide text and captions. `repair_quality` never rewrites copy: you do. **`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 tell the user that this needs the Studio plan (plans are described at https://theagentcy.app/en/pricing). Do not retry, and do not pitch the upgrade | | `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. ## 13. Read actual publishing commitments Use `get_publishing_queue({from?, to?, accountId?, limit?})` to read delivery jobs, destination accounts, frozen versions, errors and recurring posting times. Dates are ISO timestamps with UTC offsets; the range is at most 93 days. The response says when it is truncated. An unavailable read does not mean an empty queue. `list_calendar` and `set_piece_meta({plannedDate})` are editorial planning. They do not confirm a delivery time. The person confirms publishing, queue changes and rescheduling in the app. Read `get_publication_results` after publication to see stored observations at comparable post ages.