Update to Beevi
Snapshot Oct 27, 2026 · 00:54 UTC · version 1.1.0
Collection source: downloaded plugin package.
Instructions updated for beevi-expert
Skill instructions — Now says: **Photo rules** ("What Beevi can show") | The owner's standing rules for what a COVER may show: `place` (real / imagine / hidden — their own space, the...
Observed in instructions or declared skills. Runtime behavior has not been tested.
Up to 10 plugins free · no card required.
All fields and original values.
Skill instructions
Oct 24, 2026 · 13:41 UTC
`@[Maya](item:uuid)` | The item's reference sheet (1 image) or, for a gallery item, ALL its photos | Consistent characters/products; real property/location photos
Oct 27, 2026 · 00:54 UTC
**Photo rules** ("What Beevi can show") | The owner's standing rules for what a COVER may show: `place` (real / imagine / hidden — their own space, the `location` items tagged `My business`), `owner` (ai_scenes / real_only / hidden — the...
Compare saved observations
Download comparison JSONSKILL.md text changes
− removed · + added
--- before +++ after @@ -56,8 +56,10 @@ |---|---|---| | **Workspace / business** | One brand. All tools are scoped to a workspace (business). | `list_workspaces`, `get_workspace` | | **Brand profile** | The brand's identity: description, voice, tone, preferred/banned phrases, colors, fonts, target audience, competitors, goals, cover-image guidelines, **language**. | `get_brand_profile` | +| **Photo rules** ("What Beevi can show") | The owner's standing rules for what a COVER may show: `place` (real / imagine / hidden — their own space, the `location` items tagged `My business`), `owner` (ai_scenes / real_only / hidden — their face), `product_style`, `work` (real / hidden), `ai_people` (yes / no). Absent = never chosen. They limit what an image shows, never which topics get planned; covers, carousels and Compass obey them on their own. Change them only with `update_photo_rules`, and only when the owner explicitly asks ("don't put me in posts") — never infer one. | `photoRules` in `get_brand_profile` | | **Memory** | A stored fact the brand wants recalled when generating — menu items, pricing, awards, taglines, policies. The richest source for "post about our X". | `list_memories`, `get_memory` | | **Weekly Compass** | The brand's weekly "what to consider this week" signals: rising `search_trends` (each with a `post_angle`), recent `relevant_content` articles, `calendar_events`, and a markdown `overview` digest. The best source for "what should I post about right now?" / trend-driven ideas. Read-only; may be empty if the brand hasn't opted in. | `get_compass` | +| **Performance** | How published posts actually did, per platform (views, engagements, engagement rate, followers, best times to post). `get_analytics_summary` is the overview: `status` + `next_step` when there are no numbers (`connect`, `reconnect`, `unavailable` or `waiting` — say the next step), a `summary` per metric with `change` / `change_kind`, top and lowest posts, `best_times` and `unavailable` notes (add `include_daily: true` only if you need the daily series). `list_post_performance` ranks posts (`sort`: `usual`, `usual_asc`, `views`, `engagements`, `engagement_rate`, `date`); `vs_usual` 1.0 = a typical post for that account, above 1 beat it. `vs_usual` and `change` can also be `null` when there is not enough history to compare. `get_post_performance` takes a `published_post_id` or a `post_id` and returns the numbers plus the growth curve (with a `post_id`, platforms without numbers come back in `missing`: `not_synced_yet` or `not_tracked`). `last_synced_at: null` (or a "Not synced yet" note in `unavailable`) means the first sync is still pending — numbers arrive within a few hours of publishing. On a synced post a `null` metric means the platform did not report it, never 0 — do not average it in or say "no engagement". Call `get_analytics_summary` before recommending what to post more of; use `list_post_performance` with `sort: usual` for the best posts. Free on every plan; `get_post` / `list_posts` (with `include_content`) also carry a `performance` block. | `get_analytics_summary`, `list_post_performance`, `get_post_performance` | | **Post idea** | One unit of content as the user thinks of it: prompt, cover image, schedule time, status. | `list_posts`, `get_post` | | **Social post** | A per-platform variant of a post idea (LinkedIn copy ≠ Instagram copy). Each has its own status: drafted / pending_approval / approved / scheduled / sent / failed. | inside `get_post` | | **Campaign** | A themed sequence of posts over a duration (e.g. "2-week menu campaign" → ~8 posts). | `list_campaigns`, `get_campaign` | @@ -66,6 +68,7 @@ | **Style** | A saved visual style (e.g. "Watercolor") referenced by name+description in prompts. | `list_styles` | | **Visual template** | A reusable cover *overlay* (text/logo/badges) — the business's saved templates + built-in starter presets. Apply one onto a post cover with `apply_visual_template`. | `list_visual_templates` | | **Carousel** | A multi-slide image post for LinkedIn/Instagram. You can let the AI plan the deck, or write each slide yourself. | `list_carousels`, `get_carousel`, `generate_carousel`, `edit_carousel_slide`, `attach_carousel` | +| **Recipe** (post format) | A proven post format for the brand's niche (idea, angle, cover look; carousel recipes also carry a slide arc). Owners browse them on the Inspiration page and Compass plans from them. `list_recipes` lists them (8 per page, `niche` for another kind of business); `generate_post` with a `recipe_id` makes a post in one, always with an AI cover (costs credits). See "Post ideas from formats" below. | `list_recipes`, `generate_post` (`recipe_id`) | | **Job** | Long-running generation runs asynchronously; poll with `get_jobs`. | `get_jobs` | ### The two kinds of collection (the key Visuals distinction) @@ -102,7 +105,7 @@ | Mention | Attaches | Use for | |---|---|---| -| `@[Maya](item:uuid)` | The item's reference sheet (1 image) or, for a gallery item, ALL its photos | Consistent characters/products; real property/location photos | +| `@[Maya](item:uuid)` | The item's reference sheet (1 image) or, for a gallery item, ALL its photos | Consistent characters/products; real property/location photos, app screenshots | | `@[Photo 3](item_image:uuid)` | One specific gallery photo | When only one photo of a place is wanted | | `@[Products](collection:uuid)` | Every ready sheet in the collection | "Show our whole product line" — sheet collections only, never gallery collections | | `@[Brand Logo](asset:00000000-0000-0000-0000-000000000001)` | The brand logo image | Putting the logo in a generated image | @@ -376,6 +379,12 @@ rooftop bars' award memory → a 'golden hour at Top of the Gate' reel"). 3. Generate nothing until the user picks. +### "Post ideas from formats" (inspiration — reads only until the user picks) +When the user asks for post ideas or inspiration, call `list_recipes` (proven post formats for +their niche). Show a few and let them pick. Before generating, ask up to 3 short questions about +what the chosen format needs (their product, a photo, a detail), and say the AI cover costs +credits. Then `generate_post` with that format's `recipe_id` and their answers in `prompt`. + ### "Does this sound like us?" / "Make it more our voice" `get_brand_profile` → check against voice_description, tone, banned phrases — quote which rule the draft violates. Rewriting copy text is free (it's your text edit); @@ -421,4 +430,5 @@ | Reference overflow | Collection/gallery tags spread into many images against a ~14-image model cap. Tag the few that matter. | | Post idea vs social post | "The post failed" usually means ONE platform variant failed; check per-platform statuses in `get_post` before regenerating everything. | | Publish ≠ schedule | Publishing is public and immediate. Never publish when asked to schedule; never publish without an explicit confirmation. | +| A hidden owner or place | `owner: hidden` or `place: hidden` means covers will never show them — don't tag that person or a `My business` location, and don't promise a photo of them. Say the rule is on and offer to change it with `update_photo_rules` if they ask. | | Overwriting brand identity | `update_brand_*` tools replace fields. Read-modify-write, never write blind. |
Full technical diff · 1 changed fields
changed /skill_md_contents
"---\nname: beevi-expert\ndescription: >\n Fundamentals of Beevi for agents driving it through the Beevi MCP tools. Beevi is an\n AI branding agency for SMBs — it learns a business's brand and creates, schedules, and\n publishes social content (posts, campaigns, cover images, carousels, videos) across\n LinkedIn (personal profile and Company Page), X, Instagram, Facebook, TikTok, YouTube,\n Google Business Profile. Use this skill whenever a request\n involves a business's social content or brand: creating or scheduling posts, generating\n cover images or other visuals, planning campaigns, brainstorming post ideas, using or\n improving the brand identity/voice, or working with the business's visual collections\n (products, characters, properties). Trigger even when Beevi isn't named — e.g. \"make a\n post about our main dish\", \"generate a cover for the jacuzzi post\", \"plan a campaign\n about our menu\", \"help me think of something to post\".\n---\n\n# Beevi Expert — operating a brand through Beevi\n\nBeevi is a SaaS branding agency for small businesses. The business's brand identity,\nknowledge, and visual references are **already in Beevi** — your job is never to invent\ncontent from thin air, but to *retrieve what the brand already knows about itself* and\ngenerate from that. An agent that skips retrieval produces generic, off-brand content\nand wastes the user's money.\n\n## The one economic rule\n\n**Reads are free — spend them lavishly. Writes cost the user credits — never spend one\non an unresolved ambiguity.**\n\nEvery `list_*` / `get_*` tool is free. Every `generate_*` / `create_*` / `publish_*`\ntool costs real credits and/or posts publicly. So: search first, ground everything,\nask when genuinely ambiguous, and only then generate. An agent that internalizes this\nrule derives the rest of this skill on its own.\n\n## The object model (what everything is)\n\n### Know which workspace you're in (do this first)\n\nYour API key is scoped one of two ways, and it changes your first move:\n\n- **Workspace-scoped** — every tool call operates on that one workspace automatically;\n `workspace_id` args are ignored. `list_workspaces` returns exactly one entry.\n- **Organization-scoped** — always the case when you connected through a directory\n (Claude's or ChatGPT's \"connect\" button runs OAuth, which mints an organization key). You\n can reach several workspaces and must pass\n `workspace_id` explicitly. If the user's request doesn't unambiguously name which\n brand (or `list_workspaces` returns more than one), **confirm the workspace with the\n user before doing anything else** — every read you make and every credit you spend\n lands in a specific brand's account, and operating on the wrong client's brand is the\n worst mistake available to you.\n\nEither way, call `list_workspaces` once at the start so you know whose brand you're\nspeaking for — don't infer it from the user's phrasing.\n\n| Object | What it is | Read with |\n|---|---|---|\n| **Workspace / business** | One brand. All tools are scoped to a workspace (business). | `list_workspaces`, `get_workspace` |\n| **Brand profile** | The brand's identity: description, voice, tone, preferred/banned phrases, colors, fonts, target audience, competitors, goals, cover-image guidelines, **language**. | `get_brand_profile` |\n| **Memory** | A stored fact the brand wants recalled when generating — menu items, pricing, awards, taglines, policies. The richest source for \"post about our X\". | `list_memories`, `get_memory` |\n| **Weekly Compass** | The brand's weekly \"what to consider this week\" signals: rising `search_trends` (each with a `post_angle`), recent `relevant_content` articles, `calendar_events`, and a markdown `overview` digest. The best source for \"what should I post about right now?\" / trend-driven ideas. Read-only; may be empty if the brand hasn't opted in. | `get_compass` |\n| **Post idea** | One unit of content as the user thinks of it: prompt, cover image, schedule time, status. | `list_posts`, `get_post` |\n| **Social post** | A per-platform variant of a post idea (LinkedIn copy ≠ Instagram copy). Each has its own status: drafted / pending_approval / approved / scheduled / sent / failed. | inside `get_post` |\n| **Campaign** | A themed sequence of posts over a duration (e.g. \"2-week menu campaign\" → ~8 posts). | `list_campaigns`, `get_campaign` |\n| **Collection** (Visuals) | A named group of visual reference items: products, characters, properties, locations. Comes in two kinds, and every item carries free-text `tags` — see below. | `list_collections`, `list_collection_items`, `get_collection_item` |\n| **Asset** (Brand Media) | A Brand Media file. `origin` says who put it there: `user` (the customer) or `generated` (a model). `list_assets` filters on it, and on `unused` (never in a post yet — entries that have been carry a `used` tag; `get_asset` prints **Last used**). | `list_assets`, `get_asset` |\n| **Style** | A saved visual style (e.g. \"Watercolor\") referenced by name+description in prompts. | `list_styles` |\n| **Visual template** | A reusable cover *overlay* (text/logo/badges) — the business's saved templates + built-in starter presets. Apply one onto a post cover with `apply_visual_template`. | `list_visual_templates` |\n| **Carousel** | A multi-slide image post for LinkedIn/Instagram. You can let the AI plan the deck, or write each slide yourself. | `list_carousels`, `get_carousel`, `generate_carousel`, `edit_carousel_slide`, `attach_carousel` |\n| **Job** | Long-running generation runs asynchronously; poll with `get_jobs`. | `get_jobs` |\n\n### The two kinds of collection (the key Visuals distinction)\n\n- **Reference sheet** (`reference_style: sheet`) — each item is ONE identity-locked\n image of a character or product. Tagging it keeps that subject looking identical\n across generations. An item is usable **only when its sheet status is `ready`**.\n- **Gallery** (`reference_style: multi_image`) — each item is a BUNDLE of real photos\n of one thing (a property, a store). Tagging the item spreads ALL its photos in as\n references; you can also tag one specific photo.\n\n**A collection's name is not evidence. Only a listed, ready item is.** Businesses often\nhave an empty \"Characters\" collection — always `list_collection_items` and check status\nbefore promising or tagging anything.\n\n### Item tags\n\nEvery item carries up to 10 free-text `tags`, returned by both `list_collection_items` and\n`get_collection_item`. They are labels you can reason about — the suggested vocabulary per\ncollection type is spelled out in those tools' own descriptions.\n\n- **`My business`** is the one tag code keys on: it marks the location item that IS the\n business's own premises (written during onboarding, when the owner is asked \"which of these\n is your business?\"). That is how you find \"our shop\" / \"our office\" for a post — read the tag,\n don't guess from item names.\n- `manage_collection_item` writes them **replace-not-merge**, same as `metadata` — call\n `get_collection_item` first and send back the full list, or you will silently drop tags.\n\n## @-mentions: how you wire brand material into generation\n\nPrompts passed to generation tools support `@[Name](scheme:uuid)` mentions. This is how\nretrieved material actually reaches the model — a mention is not decoration, it attaches\nthe real reference:\n\n| Mention | Attaches | Use for |\n|---|---|---|\n| `@[Maya](item:uuid)` | The item's reference sheet (1 image) or, for a gallery item, ALL its photos | Consistent characters/products; real property/location photos |\n| `@[Photo 3](item_image:uuid)` | One specific gallery photo | When only one photo of a place is wanted |\n| `@[Products](collection:uuid)` | Every ready sheet in the collection | \"Show our whole product line\" — sheet collections only, never gallery collections |\n| `@[Brand Logo](asset:00000000-0000-0000-0000-000000000001)` | The brand logo image | Putting the logo in a generated image |\n| `@[Photo](asset:uuid)` | That asset (either origin) | Any Brand Media file as visual reference |\n| `@[Watercolor](style:uuid)` | **Text only** (name + description, no images) | Visual style direction |\n| `@[Pricing](memory:uuid)` | **Text only** (the fact, inlined) | Grounding copy in a stored fact |\n\nSurface limits worth knowing. A whole gallery item attaches **every photo it has**, so where\nit may go depends on how many references that surface can hold:\n\n- **image / cover / video** — fine, it spreads.\n- **carousel deck `prompt`** — fine, and it is the good move: the item becomes a *catalogue*\n and the planner puts the photo that fits onto each slide. This is how you say \"use our real\n photos\".\n- **a single carousel slide, or `edit_carousel_slide`** — refused (every photo onto one image).\n Tag ONE photo there with `@[Name](item_image:UUID)` from `get_collection_item`.\n- **post body / campaign text** — refused; that text is written from words, not pictures.\n\n`item_image:` is one reference, so it is welcome anywhere an image is rendered. Image models\ncap total references (~14) — tag the few that matter.\n\n## The loop: Resolve → Ground → Gate → Execute\n\nRun this loop for every content request. The common failure mode is skipping straight\nto Execute.\n\n### 1. Resolve the subject (free)\n\n\"The jacuzzi\", \"our main dish\", \"the new sunscreen\" — the subject almost always already\nexists somewhere in Beevi. Search, in parallel:\n\n- `list_memories` — facts (\"Joe's is known for 18-inch NY-style pies\")\n- `list_collections` + `list_collection_items` — visual references (a \"Radiance Serum\"\n product sheet, a property gallery)\n- `list_posts` — existing posts (\"the post about the jacuzzi\" → find that actual post). Also\n filterable by `lifecycle` — `needs_attention` (awaiting approval / overdue / declined / failed\n generation / partially published — a platform leg failed), `upcoming` (scheduled), `drafts` (draft or\n still generating), `published` (fully published) — to answer \"what needs my attention\" or \"what's\n going out next\" in one call instead of scanning everything. A post whose scheduled time passed\n without ever generating (e.g. the org ran out of credits) shows up here as `generation_failed`\n with `publish_error`: *\"Not generated before its scheduled time (e.g. out of credits). Open it and\n click Generate.\"* — explain that plainly and offer to `generate_post` it now, or point at a top-up\n if credits are the actual cause.\n- `list_assets` — the customer's photos (`origin: 'user'`) or model output (`origin: 'generated'`); `unused: true` narrows to photos that have never been on a post — the pool to pick a fresh cover reference from, so the same product shot is not reused week after week (a photo counts as used once a post carries it, or carries an AI image made from it)\n\nSearch results are shallow — **open the top candidates with `get_post` /\n`get_collection_item` before concluding something doesn't exist.** The subject often\nlives inside a platform variant's body text, not in the title or search index, and a\n\"no such post exists\" verdict you didn't verify is worse than a wrong guess: it sends\nthe user to re-explain something you already had.\n\nWhile you're in `list_posts`, also check for **existing content about the same\nsubject** — an unsent draft about the same product means your job may be to revive or\nupdate it, not to pay for a duplicate.\n\nThe subject may live in one, several, or none of these. Multiple plausible matches\n(three sunscreen products) → that's a question for the user, not a guess.\n\n### 2. Ground in the brand (free, never skip)\n\nCall `get_brand_profile` before generating anything. (Generation tools do inject brand\ncontext server-side, but that doesn't replace reading the profile yourself — you need\nit to write the prompt in the right language, honor banned phrases, and know the cover\nrules *before* you spend, not discover them in the output.) Apply:\n\n- **Language** — generate in the brand's language. A Hebrew brand gets Hebrew posts.\n The voice_description may itself be written in that language — that IS the voice;\n don't translate it away or default to English.\n- **Voice & tone** — write like the brand, not like an assistant.\n- **Banned/preferred phrases** — hard constraints on copy.\n- **Cover guidelines, colors, logo position** — hard constraints on cover images.\n- **Target audience & goals** — pick angles that serve them.\n\n### 3. Gate: the pre-spend checklist (this is where you ask)\n\nBefore ANY credit-spending call, you must be able to name all three:\n\n1. **The resolved subject, with evidence** — a specific memory / item / post / asset ID,\n or an explicit \"nothing found — proceeding on brand identity plus the user's words\".\n2. **The target** — which platforms, or which existing post the cover is for.\n3. **The visual grounding** — which mentions you'll attach, or an explicit\n \"no relevant visuals exist\".\n\nAny of the three missing or ambiguous → ask the user. Bundle everything into **one\nmessage with at most ~2 questions**; offer the concrete options you found (\"I found\nthree sunscreens: PROTECT ME, PROTECT BODY, GLOW ME+ — which one?\"). If all three are\nresolved, do NOT ask permission to proceed — act.\n\n**Show your grounding** as you proceed, in one line: *\"Found the jacuzzi in the\nrooftop-spa memory and 4 photos in the 'Amenities' gallery — using those.\"* This is the\ncheapest way for the user to catch a wrong resolution before credits burn.\n\n### 4. Execute (costs credits)\n\nPick the narrowest tool that does the job, wire in the mentions, then poll `get_jobs`\nif the run is async, and report what was created (with links/IDs).\n\n## Per-intent playbooks\n\n### \"Generate a cover image for the post about X\"\n1. `list_posts` → find the actual post about X (it usually exists — that's why they said \"the post\").\n2. Resolve X's visuals: gallery photos of the real thing beat AI imagination — a real\n jacuzzi photo tagged as reference makes a truthful cover.\n3. `get_brand_profile` → cover guidelines, colors, logo position.\n4. Gate, then `generate_cover_image` for that post with the mentions attached.\n5. If the post **already has a cover**, replacing it is a destructive-ish edit — say\n you're replacing it (or ask, if the user only said \"make a cover\" without naming\n the post confidently).\n\n### \"Create a post about our X\" (main dish, new product, a property…)\n1. Resolve X across memories / collections / assets. The best post copy is grounded in\n a real fact (\"late-night weekend hours\", \"$340/year\") — tag the memory.\n2. `get_brand_profile` → language + voice.\n3. Gate (platforms? one product or several matches?).\n4. `generate_post` (full agent run: copy + platform variants + cover) or `create_post`\n (you write the content). Prefer `generate_post` unless the user dictated exact copy.\n To make it a **carousel**, pass `post_type: 'carousel'` — the one post job also generates\n and attaches a branded multi-slide carousel on carousel-capable platforms (costs extra\n carousel credits; falls back to a plain post if the platforms can't host one).\n\n### \"Make a carousel with these slides\" (the user dictates the content)\n**First check whose pictures these are.** If the user handed you actual photos, they want those\nphotos posted — see the next playbook. `generate_carousel` *invents* artwork and costs credits;\nreach for it only when new pictures are what's wanted.\n\n`generate_carousel` has two modes. Given only a topic, an AI planner writes the deck.\nBut when the user tells you what the slides should say — or you have researched the\nsubject and know — pass `slides[]` and write them yourself; the planner is skipped and\ntheir wording survives verbatim.\n\nEach slide is `{ text, visual }`:\n- `text` — every word to render ON the slide, in quotes, with placement where it\n matters: `\"Best in town\" large in the middle, \"Guaranteed\" small bottom-left`. Write\n the words, never a label for them (\"Headline: …\" renders the word \"Headline\").\n- `visual` — what the slide SHOWS, subject only. Don't put palette or fonts here; those\n come from `visual_style` (deck-level, optional — omit it and the brand profile\n supplies one). Per-slide styling is how a deck ends up looking like three decks.\n- Tag a product with `@[Name](item:UUID)` inside `visual` to put its real photo on that\n slide. Sheet items only.\n\n**Where the words live — `text_mode`.** By default the image model paints the words into\neach picture. Pass `text_mode: 'dynamic'` and it shoots the photographs text-free instead,\nthen lays the copy over them as real text in the brand font: crisp type at any size, no\nmisspelled words, and the user can retype or move it afterwards in the Carousel Maker. Good\nfor a wordy deck, a brand that cares about its typography, or any language the image model\nspells badly. It cannot be combined with `slides[]` or `template_id` — a dynamic deck's copy\nis written by the planner alongside the photographs it has to fit, and a template already\ndefines where text sits. Pass `prompt` alone.\n\nTo change a deck afterwards, `edit_carousel_slide` handles one slide: `instruction`\nfor the look (\"make it darker\"), `text` for new words, or both. Changing several slides\nmeans several calls — in the in-app chat they run in parallel when emitted in one turn.\nAn edit keeps the references the slide already has, and a mention in `instruction` attaches\nits photo too — `@[Name](item:UUID)` for a sheet product, `@[Name](item_image:UUID)` for one\nreal photo (this is how \"use the actual photo of our entrance\" is honoured), `asset:` for\nBrand Media. Say how to use it: \"as the background, exactly as-is\". Tag sparingly — the\nslide's own references and yours share one per-model budget, so tagging several can push the\nslide's existing subjects out of the frame.\n\nOn a **dynamic** deck `text` is refused — the words are a layer over the photo, not part of\nit, so rewriting them through the image model would leave the slide with two headlines.\n`instruction` works normally there and is usually all you need: it replaces the picture and\nthe existing words re-fit themselves to it. To change the *words*, use `set_cover_text` with\nthe `carousel_id` and `slide_number` — free, instant, no new picture.\n\n### Saving the user's photos to Brand Media — always describe them yourself\n\n`upload_asset` (a URL) and `get_upload_token` (local files via curl) put the user's own photos into\nBrand Media. You have usually SEEN the photo, so pass **`visual_description`**: 2-4 factual\nsentences, and say FIRST if the photo carries any text, logo or signage. Then it costs the user\nnothing. Without it, a workspace that uses its own photos as covers pays for Beevi to look at the\nphoto (about 1 credit per 70 photos) and the tool result carries a `note` saying so. With curl, put\neach `-F visual_description=…` immediately after its `-F file=@…`. To describe a photo that is\nalready uploaded, `update_asset({ asset_id, visual_description })`.\n\n### \"Put these photos on the post\" (the user's OWN pictures) — `set_post_photos`\n\nWhen the user hands you photos — chat attachments, which arrive with an `asset_id`, or ids from\n`list_assets` — and wants them published, that is `set_post_photos`. **Free and instant.** It\ndoes not generate, edit or restyle anything; the photos go up exactly as supplied. This is the\nanswer to \"make a carousel out of these\", \"post these five shots\", \"use my photos instead\".\n\n```\nset_post_photos({ post_id | social_post_ids, photo_asset_ids: [...] })\n```\n\n- **The order you pass IS the slide order.** Nothing re-sorts them.\n- Target `post_id` for every photo-capable platform of the post, or `social_post_ids` for\n specific platform versions. Exactly one of the two — not both, not neither. All the ids must\n belong to one post.\n- **REPLACE, not append.** It swaps out whatever media those posts have. To *add* photos, call\n `get_post` first and pass the existing ones plus the new ones.\n- **Photos only.** A post holds photos OR one video, never both — a video in the set is refused.\n To set a video cover, use `update_social_post`'s `cover_media`.\n- Each platform version is trimmed to its own limit (x 4, google_business 1, facebook/instagram\n 10, linkedin 20, tiktok 35) and the result reports `trimmed_from`. Published posts and\n platforms that can't take photos come back in `skipped[]` with a reason. **Tell the user in\n one line what was trimmed or skipped** — the card doesn't show it.\n- Editing a post this way **re-opens its approval** if it was scheduled.\n\n### Changing the words ON a cover — `get_cover_text` / `set_cover_text`\n\nA **dynamic** cover carries its headline as real text over the photo, so rewording it is a\ndatabase write: free, instant, no image regeneration, no credits. An **embedded** cover has\nthe words painted into the pixels and nothing can edit them in place.\n\n`get_cover_text({ post_id | social_post_id | carousel_id, slide_number? })` **is the test for\nwhich one you have** — call it first on any \"change what the cover says\" request:\n\n- `text_nodes` present → editable. Reword with `set_cover_text`.\n- `text_nodes` **empty** → baked into the photo. The only way to change them is to\n regenerate the picture (`update_post({ generate_cover_prompt })` for a post cover,\n `edit_carousel_slide({ slide_number, text })` for a deck slide). Both cost credits and\n produce a *different* picture — say so and get a go-ahead first.\n\n`set_cover_text` takes either `find` + `replace` (prefer it when the user quoted the wording\n— it cannot rewrite the words around the ones they asked about) or `edits: [{node_id, text}]`\naddressed by ids `get_cover_text` returned. **Never invent a node id**, and note it can only\nreword lines that already exist — adding a new subheadline is an editor job. Defaults to\nevery platform of a post / every slide of a deck; narrow with `social_post_id` or\n`slide_number`. Refused on published posts and video covers.\n\n**Order matters:** edit the deck *before* `attach_carousel`. Attaching copies the slides\ninto the post, and later slide edits don't flow through to a post already attached.\n\n### The first comment under a post — `get_auto_comments` / `set_auto_comments`\n\nBeevi can publish a comment under its own post the moment that post goes live — the\n\"link in first comment\" play. `set_auto_comments({ post_id | social_post_id, comments:\n[{content, delay_minutes}] })` **REPLACES the whole list**, so call `get_auto_comments`\nfirst when editing one of several; use `mode: 'append'` to add one without touching the\nrest. No comment id ever crosses the wire, which is deliberate — there is none to invent.\n\nWrite each platform in its own idiom, and note these are platform facts, not house style:\na real clickable URL on LinkedIn, Facebook and X (keep X under 280 characters); on\nInstagram a URL in a comment is dead text, so point at the bio instead. TikTok, YouTube and\nGoogle Business cannot take one at all, and Facebook/Instagram are gated off in production\npending Meta's review — the tool says which when it refuses.\n\n**Only before the post publishes.** The comment's timer starts at the real publish moment,\nso a post that is already live can no longer take one and the tool will refuse. Schedule\nthe comment while you schedule the post. `delay_minutes: 0` is right for a link — it should\nbe there when readers arrive.\n\n### \"Generate a campaign about our X\" (menu, product line, event series)\n1. Resolve X — a campaign needs *enough material*: several memories/items to spread\n across posts. List what you found and how it maps to a sequence.\n2. Gate: duration, cadence/post count, platforms, content types (text / +carousel /\n +video) — these are exactly the ambiguities worth one bundled question.\n3. `create_campaign`. Report the plan, don't just say \"done\". Two modes: for an open\n ask, use `brief` mode (put the cadence wording in `brief`, compute `post_count`\n yourself) so the campaign agent plans it; if the plan was already shaped in chat,\n pass pre-planned `posts[]` instead for an instant, no-planning-credit create.\n\n### \"Rename / pause / delete the campaign\" · \"move this post into the campaign\"\n- `manage_campaign` action `update`: title, description, period, and pause/resume\n (`status` — only active↔paused transitions are allowed).\n- `manage_campaign` action `delete` is PERMANENT — confirm with the user first, and\n state what happens to its posts: `delete_posts=false` (default) keeps them detached;\n `=true` also deletes them and is refused if any is already published.\n- Move a post in/out of a campaign: `update_post` with `campaign_id` (a campaign id\n attaches, `null` detaches). Campaign dates do NOT auto-adjust to the moved post.\n- Delete a whole post (all platforms): `delete_post` — permanent, confirm first;\n refused if any platform is already published (use `delete_social_post` for the\n unpublished platforms instead).\n\n### \"Help me think of what to post\" (ideation — reads only, free)\n1. Pull broadly: brand profile, memories, collections, recent posts (to avoid\n repeating), upcoming season/dates.\n2. Offer ~5 concrete, brand-specific angles, each naming its grounding (\"Your 'Top 10\n rooftop bars' award memory → a 'golden hour at Top of the Gate' reel\").\n3. Generate nothing until the user picks.\n\n### \"Does this sound like us?\" / \"Make it more our voice\"\n`get_brand_profile` → check against voice_description, tone, banned phrases — quote\nwhich rule the draft violates. Rewriting copy text is free (it's your text edit);\nregenerating a post via tools is not.\n\n### \"Schedule / publish\"\n- Scheduling = `update_post` with a time (respect the business's timezone).\n- Scheduling to a platform the workspace has not connected (or whose token needs a\n reconnect) still succeeds, but the result carries `unconnected_platforms` + a `warning`\n — relay it verbatim: the publish will fail unless the user connects it in Integrations\n before the slot. `get_workspace.connected_platforms` tells you up front.\n- `publish_post` posts publicly and irreversibly → echo exactly what will go out,\n where, and when, and get an explicit yes first. Same for `approve_post` when\n approvals are on: approving is a decision that belongs to the user.\n- Once the org's due posts have scheduled generation working, the publisher picks up a\n `scheduled` post within ~15s of its slot — but only once the publisher process is live\n (`SCHEDULED_PUBLISH_MODE=publisher`). Until then the every-minute cron publishes within\n ~1 min, so a delay of a minute or two is normal; \"why hasn't it published yet\" a minute\n after the slot is a real problem once the publisher is live, not before. A post you\n reschedule or unschedule after the publisher has already picked it up for this tick is\n simply skipped by that tick, not double-published — the new time takes effect on the\n next sweep.\n- Auto-generation for an org that's out of credits pauses for up to an hour (not forever) and\n resumes immediately the moment credits change (top-up, plan change, trial extension) — so\n \"buy credits\" is a real fix, not just advice, if that's what `generation_failed` turns out\n to be.\n\n### \"Remember that we never say X\" / brand corrections\nDurable facts → `create_memory`. Actual identity changes (voice, audience, banned\nphrases) → the `update_brand_*` tools — but read `get_brand_profile` first and change\nonly the field in question; these tools overwrite.\n\n## Gotchas (each one is a real observed failure)\n\n| Trap | Rule |\n|---|---|\n| Empty \"Characters\" collection | A collection name is not evidence; list its items and check `ready` status before tagging or promising. |\n| Wrong language | The brand's `language` field governs output language. Hebrew brand → Hebrew post, RTL-safe. Never default to English. |\n| Generic content | If you didn't cite a memory/item/post in your grounding line, you probably invented something. Go back to Resolve. |\n| No visuals exist | Many brands have zero collections. Say so and proceed with brand identity + logo — don't fake a reference or stall. |\n| Gallery item in post text | Gallery items are for image/video/cover generation only; post-body and carousel generation reject them. |\n| Tagging a gallery *collection* | Never taggable — tag its individual items. |\n| Reference overflow | Collection/gallery tags spread into many images against a ~14-image model cap. Tag the few that matter. |\n| Post idea vs social post | \"The post failed\" usually means ONE platform variant failed; check per-platform statuses in `get_post` before regenerating everything. |\n| Publish ≠ schedule | Publishing is public and immediate. Never publish when asked to schedule; never publish without an explicit confirmation. |\n| Overwriting brand identity | `update_brand_*` tools replace fields. Read-modify-write, never write blind. |\n""---\nname: beevi-expert\ndescription: >\n Fundamentals of Beevi for agents driving it through the Beevi MCP tools. Beevi is an\n AI branding agency for SMBs — it learns a business's brand and creates, schedules, and\n publishes social content (posts, campaigns, cover images, carousels, videos) across\n LinkedIn (personal profile and Company Page), X, Instagram, Facebook, TikTok, YouTube,\n Google Business Profile. Use this skill whenever a request\n involves a business's social content or brand: creating or scheduling posts, generating\n cover images or other visuals, planning campaigns, brainstorming post ideas, using or\n improving the brand identity/voice, or working with the business's visual collections\n (products, characters, properties). Trigger even when Beevi isn't named — e.g. \"make a\n post about our main dish\", \"generate a cover for the jacuzzi post\", \"plan a campaign\n about our menu\", \"help me think of something to post\".\n---\n\n# Beevi Expert — operating a brand through Beevi\n\nBeevi is a SaaS branding agency for small businesses. The business's brand identity,\nknowledge, and visual references are **already in Beevi** — your job is never to invent\ncontent from thin air, but to *retrieve what the brand already knows about itself* and\ngenerate from that. An agent that skips retrieval produces generic, off-brand content\nand wastes the user's money.\n\n## The one economic rule\n\n**Reads are free — spend them lavishly. Writes cost the user credits — never spend one\non an unresolved ambiguity.**\n\nEvery `list_*` / `get_*` tool is free. Every `generate_*` / `create_*` / `publish_*`\ntool costs real credits and/or posts publicly. So: search first, ground everything,\nask when genuinely ambiguous, and only then generate. An agent that internalizes this\nrule derives the rest of this skill on its own.\n\n## The object model (what everything is)\n\n### Know which workspace you're in (do this first)\n\nYour API key is scoped one of two ways, and it changes your first move:\n\n- **Workspace-scoped** — every tool call operates on that one workspace automatically;\n `workspace_id` args are ignored. `list_workspaces` returns exactly one entry.\n- **Organization-scoped** — always the case when you connected through a directory\n (Claude's or ChatGPT's \"connect\" button runs OAuth, which mints an organization key). You\n can reach several workspaces and must pass\n `workspace_id` explicitly. If the user's request doesn't unambiguously name which\n brand (or `list_workspaces` returns more than one), **confirm the workspace with the\n user before doing anything else** — every read you make and every credit you spend\n lands in a specific brand's account, and operating on the wrong client's brand is the\n worst mistake available to you.\n\nEither way, call `list_workspaces` once at the start so you know whose brand you're\nspeaking for — don't infer it from the user's phrasing.\n\n| Object | What it is | Read with |\n|---|---|---|\n| **Workspace / business** | One brand. All tools are scoped to a workspace (business). | `list_workspaces`, `get_workspace` |\n| **Brand profile** | The brand's identity: description, voice, tone, preferred/banned phrases, colors, fonts, target audience, competitors, goals, cover-image guidelines, **language**. | `get_brand_profile` |\n| **Photo rules** (\"What Beevi can show\") | The owner's standing rules for what a COVER may show: `place` (real / imagine / hidden — their own space, the `location` items tagged `My business`), `owner` (ai_scenes / real_only / hidden — their face), `product_style`, `work` (real / hidden), `ai_people` (yes / no). Absent = never chosen. They limit what an image shows, never which topics get planned; covers, carousels and Compass obey them on their own. Change them only with `update_photo_rules`, and only when the owner explicitly asks (\"don't put me in posts\") — never infer one. | `photoRules` in `get_brand_profile` |\n| **Memory** | A stored fact the brand wants recalled when generating — menu items, pricing, awards, taglines, policies. The richest source for \"post about our X\". | `list_memories`, `get_memory` |\n| **Weekly Compass** | The brand's weekly \"what to consider this week\" signals: rising `search_trends` (each with a `post_angle`), recent `relevant_content` articles, `calendar_events`, and a markdown `overview` digest. The best source for \"what should I post about right now?\" / trend-driven ideas. Read-only; may be empty if the brand hasn't opted in. | `get_compass` |\n| **Performance** | How published posts actually did, per platform (views, engagements, engagement rate, followers, best times to post). `get_analytics_summary` is the overview: `status` + `next_step` when there are no numbers (`connect`, `reconnect`, `unavailable` or `waiting` — say the next step), a `summary` per metric with `change` / `change_kind`, top and lowest posts, `best_times` and `unavailable` notes (add `include_daily: true` only if you need the daily series). `list_post_performance` ranks posts (`sort`: `usual`, `usual_asc`, `views`, `engagements`, `engagement_rate`, `date`); `vs_usual` 1.0 = a typical post for that account, above 1 beat it. `vs_usual` and `change` can also be `null` when there is not enough history to compare. `get_post_performance` takes a `published_post_id` or a `post_id` and returns the numbers plus the growth curve (with a `post_id`, platforms without numbers come back in `missing`: `not_synced_yet` or `not_tracked`). `last_synced_at: null` (or a \"Not synced yet\" note in `unavailable`) means the first sync is still pending — numbers arrive within a few hours of publishing. On a synced post a `null` metric means the platform did not report it, never 0 — do not average it in or say \"no engagement\". Call `get_analytics_summary` before recommending what to post more of; use `list_post_performance` with `sort: usual` for the best posts. Free on every plan; `get_post` / `list_posts` (with `include_content`) also carry a `performance` block. | `get_analytics_summary`, `list_post_performance`, `get_post_performance` |\n| **Post idea** | One unit of content as the user thinks of it: prompt, cover image, schedule time, status. | `list_posts`, `get_post` |\n| **Social post** | A per-platform variant of a post idea (LinkedIn copy ≠ Instagram copy). Each has its own status: drafted / pending_approval / approved / scheduled / sent / failed. | inside `get_post` |\n| **Campaign** | A themed sequence of posts over a duration (e.g. \"2-week menu campaign\" → ~8 posts). | `list_campaigns`, `get_campaign` |\n| **Collection** (Visuals) | A named group of visual reference items: products, characters, properties, locations. Comes in two kinds, and every item carries free-text `tags` — see below. | `list_collections`, `list_collection_items`, `get_collection_item` |\n| **Asset** (Brand Media) | A Brand Media file. `origin` says who put it there: `user` (the customer) or `generated` (a model). `list_assets` filters on it, and on `unused` (never in a post yet — entries that have been carry a `used` tag; `get_asset` prints **Last used**). | `list_assets`, `get_asset` |\n| **Style** | A saved visual style (e.g. \"Watercolor\") referenced by name+description in prompts. | `list_styles` |\n| **Visual template** | A reusable cover *overlay* (text/logo/badges) — the business's saved templates + built-in starter presets. Apply one onto a post cover with `apply_visual_template`. | `list_visual_templates` |\n| **Carousel** | A multi-slide image post for LinkedIn/Instagram. You can let the AI plan the deck, or write each slide yourself. | `list_carousels`, `get_carousel`, `generate_carousel`, `edit_carousel_slide`, `attach_carousel` |\n| **Recipe** (post format) | A proven post format for the brand's niche (idea, angle, cover look; carousel recipes also carry a slide arc). Owners browse them on the Inspiration page and Compass plans from them. `list_recipes` lists them (8 per page, `niche` for another kind of business); `generate_post` with a `recipe_id` makes a post in one, always with an AI cover (costs credits). See \"Post ideas from formats\" below. | `list_recipes`, `generate_post` (`recipe_id`) |\n| **Job** | Long-running generation runs asynchronously; poll with `get_jobs`. | `get_jobs` |\n\n### The two kinds of collection (the key Visuals distinction)\n\n- **Reference sheet** (`reference_style: sheet`) — each item is ONE identity-locked\n image of a character or product. Tagging it keeps that subject looking identical\n across generations. An item is usable **only when its sheet status is `ready`**.\n- **Gallery** (`reference_style: multi_image`) — each item is a BUNDLE of real photos\n of one thing (a property, a store). Tagging the item spreads ALL its photos in as\n references; you can also tag one specific photo.\n\n**A collection's name is not evidence. Only a listed, ready item is.** Businesses often\nhave an empty \"Characters\" collection — always `list_collection_items` and check status\nbefore promising or tagging anything.\n\n### Item tags\n\nEvery item carries up to 10 free-text `tags`, returned by both `list_collection_items` and\n`get_collection_item`. They are labels you can reason about — the suggested vocabulary per\ncollection type is spelled out in those tools' own descriptions.\n\n- **`My business`** is the one tag code keys on: it marks the location item that IS the\n business's own premises (written during onboarding, when the owner is asked \"which of these\n is your business?\"). That is how you find \"our shop\" / \"our office\" for a post — read the tag,\n don't guess from item names.\n- `manage_collection_item` writes them **replace-not-merge**, same as `metadata` — call\n `get_collection_item` first and send back the full list, or you will silently drop tags.\n\n## @-mentions: how you wire brand material into generation\n\nPrompts passed to generation tools support `@[Name](scheme:uuid)` mentions. This is how\nretrieved material actually reaches the model — a mention is not decoration, it attaches\nthe real reference:\n\n| Mention | Attaches | Use for |\n|---|---|---|\n| `@[Maya](item:uuid)` | The item's reference sheet (1 image) or, for a gallery item, ALL its photos | Consistent characters/products; real property/location photos, app screenshots |\n| `@[Photo 3](item_image:uuid)` | One specific gallery photo | When only one photo of a place is wanted |\n| `@[Products](collection:uuid)` | Every ready sheet in the collection | \"Show our whole product line\" — sheet collections only, never gallery collections |\n| `@[Brand Logo](asset:00000000-0000-0000-0000-000000000001)` | The brand logo image | Putting the logo in a generated image |\n| `@[Photo](asset:uuid)` | That asset (either origin) | Any Brand Media file as visual reference |\n| `@[Watercolor](style:uuid)` | **Text only** (name + description, no images) | Visual style direction |\n| `@[Pricing](memory:uuid)` | **Text only** (the fact, inlined) | Grounding copy in a stored fact |\n\nSurface limits worth knowing. A whole gallery item attaches **every photo it has**, so where\nit may go depends on how many references that surface can hold:\n\n- **image / cover / video** — fine, it spreads.\n- **carousel deck `prompt`** — fine, and it is the good move: the item becomes a *catalogue*\n and the planner puts the photo that fits onto each slide. This is how you say \"use our real\n photos\".\n- **a single carousel slide, or `edit_carousel_slide`** — refused (every photo onto one image).\n Tag ONE photo there with `@[Name](item_image:UUID)` from `get_collection_item`.\n- **post body / campaign text** — refused; that text is written from words, not pictures.\n\n`item_image:` is one reference, so it is welcome anywhere an image is rendered. Image models\ncap total references (~14) — tag the few that matter.\n\n## The loop: Resolve → Ground → Gate → Execute\n\nRun this loop for every content request. The common failure mode is skipping straight\nto Execute.\n\n### 1. Resolve the subject (free)\n\n\"The jacuzzi\", \"our main dish\", \"the new sunscreen\" — the subject almost always already\nexists somewhere in Beevi. Search, in parallel:\n\n- `list_memories` — facts (\"Joe's is known for 18-inch NY-style pies\")\n- `list_collections` + `list_collection_items` — visual references (a \"Radiance Serum\"\n product sheet, a property gallery)\n- `list_posts` — existing posts (\"the post about the jacuzzi\" → find that actual post). Also\n filterable by `lifecycle` — `needs_attention` (awaiting approval / overdue / declined / failed\n generation / partially published — a platform leg failed), `upcoming` (scheduled), `drafts` (draft or\n still generating), `published` (fully published) — to answer \"what needs my attention\" or \"what's\n going out next\" in one call instead of scanning everything. A post whose scheduled time passed\n without ever generating (e.g. the org ran out of credits) shows up here as `generation_failed`\n with `publish_error`: *\"Not generated before its scheduled time (e.g. out of credits). Open it and\n click Generate.\"* — explain that plainly and offer to `generate_post` it now, or point at a top-up\n if credits are the actual cause.\n- `list_assets` — the customer's photos (`origin: 'user'`) or model output (`origin: 'generated'`); `unused: true` narrows to photos that have never been on a post — the pool to pick a fresh cover reference from, so the same product shot is not reused week after week (a photo counts as used once a post carries it, or carries an AI image made from it)\n\nSearch results are shallow — **open the top candidates with `get_post` /\n`get_collection_item` before concluding something doesn't exist.** The subject often\nlives inside a platform variant's body text, not in the title or search index, and a\n\"no such post exists\" verdict you didn't verify is worse than a wrong guess: it sends\nthe user to re-explain something you already had.\n\nWhile you're in `list_posts`, also check for **existing content about the same\nsubject** — an unsent draft about the same product means your job may be to revive or\nupdate it, not to pay for a duplicate.\n\nThe subject may live in one, several, or none of these. Multiple plausible matches\n(three sunscreen products) → that's a question for the user, not a guess.\n\n### 2. Ground in the brand (free, never skip)\n\nCall `get_brand_profile` before generating anything. (Generation tools do inject brand\ncontext server-side, but that doesn't replace reading the profile yourself — you need\nit to write the prompt in the right language, honor banned phrases, and know the cover\nrules *before* you spend, not discover them in the output.) Apply:\n\n- **Language** — generate in the brand's language. A Hebrew brand gets Hebrew posts.\n The voice_description may itself be written in that language — that IS the voice;\n don't translate it away or default to English.\n- **Voice & tone** — write like the brand, not like an assistant.\n- **Banned/preferred phrases** — hard constraints on copy.\n- **Cover guidelines, colors, logo position** — hard constraints on cover images.\n- **Target audience & goals** — pick angles that serve them.\n\n### 3. Gate: the pre-spend checklist (this is where you ask)\n\nBefore ANY credit-spending call, you must be able to name all three:\n\n1. **The resolved subject, with evidence** — a specific memory / item / post / asset ID,\n or an explicit \"nothing found — proceeding on brand identity plus the user's words\".\n2. **The target** — which platforms, or which existing post the cover is for.\n3. **The visual grounding** — which mentions you'll attach, or an explicit\n \"no relevant visuals exist\".\n\nAny of the three missing or ambiguous → ask the user. Bundle everything into **one\nmessage with at most ~2 questions**; offer the concrete options you found (\"I found\nthree sunscreens: PROTECT ME, PROTECT BODY, GLOW ME+ — which one?\"). If all three are\nresolved, do NOT ask permission to proceed — act.\n\n**Show your grounding** as you proceed, in one line: *\"Found the jacuzzi in the\nrooftop-spa memory and 4 photos in the 'Amenities' gallery — using those.\"* This is the\ncheapest way for the user to catch a wrong resolution before credits burn.\n\n### 4. Execute (costs credits)\n\nPick the narrowest tool that does the job, wire in the mentions, then poll `get_jobs`\nif the run is async, and report what was created (with links/IDs).\n\n## Per-intent playbooks\n\n### \"Generate a cover image for the post about X\"\n1. `list_posts` → find the actual post about X (it usually exists — that's why they said \"the post\").\n2. Resolve X's visuals: gallery photos of the real thing beat AI imagination — a real\n jacuzzi photo tagged as reference makes a truthful cover.\n3. `get_brand_profile` → cover guidelines, colors, logo position.\n4. Gate, then `generate_cover_image` for that post with the mentions attached.\n5. If the post **already has a cover**, replacing it is a destructive-ish edit — say\n you're replacing it (or ask, if the user only said \"make a cover\" without naming\n the post confidently).\n\n### \"Create a post about our X\" (main dish, new product, a property…)\n1. Resolve X across memories / collections / assets. The best post copy is grounded in\n a real fact (\"late-night weekend hours\", \"$340/year\") — tag the memory.\n2. `get_brand_profile` → language + voice.\n3. Gate (platforms? one product or several matches?).\n4. `generate_post` (full agent run: copy + platform variants + cover) or `create_post`\n (you write the content). Prefer `generate_post` unless the user dictated exact copy.\n To make it a **carousel**, pass `post_type: 'carousel'` — the one post job also generates\n and attaches a branded multi-slide carousel on carousel-capable platforms (costs extra\n carousel credits; falls back to a plain post if the platforms can't host one).\n\n### \"Make a carousel with these slides\" (the user dictates the content)\n**First check whose pictures these are.** If the user handed you actual photos, they want those\nphotos posted — see the next playbook. `generate_carousel` *invents* artwork and costs credits;\nreach for it only when new pictures are what's wanted.\n\n`generate_carousel` has two modes. Given only a topic, an AI planner writes the deck.\nBut when the user tells you what the slides should say — or you have researched the\nsubject and know — pass `slides[]` and write them yourself; the planner is skipped and\ntheir wording survives verbatim.\n\nEach slide is `{ text, visual }`:\n- `text` — every word to render ON the slide, in quotes, with placement where it\n matters: `\"Best in town\" large in the middle, \"Guaranteed\" small bottom-left`. Write\n the words, never a label for them (\"Headline: …\" renders the word \"Headline\").\n- `visual` — what the slide SHOWS, subject only. Don't put palette or fonts here; those\n come from `visual_style` (deck-level, optional — omit it and the brand profile\n supplies one). Per-slide styling is how a deck ends up looking like three decks.\n- Tag a product with `@[Name](item:UUID)` inside `visual` to put its real photo on that\n slide. Sheet items only.\n\n**Where the words live — `text_mode`.** By default the image model paints the words into\neach picture. Pass `text_mode: 'dynamic'` and it shoots the photographs text-free instead,\nthen lays the copy over them as real text in the brand font: crisp type at any size, no\nmisspelled words, and the user can retype or move it afterwards in the Carousel Maker. Good\nfor a wordy deck, a brand that cares about its typography, or any language the image model\nspells badly. It cannot be combined with `slides[]` or `template_id` — a dynamic deck's copy\nis written by the planner alongside the photographs it has to fit, and a template already\ndefines where text sits. Pass `prompt` alone.\n\nTo change a deck afterwards, `edit_carousel_slide` handles one slide: `instruction`\nfor the look (\"make it darker\"), `text` for new words, or both. Changing several slides\nmeans several calls — in the in-app chat they run in parallel when emitted in one turn.\nAn edit keeps the references the slide already has, and a mention in `instruction` attaches\nits photo too — `@[Name](item:UUID)` for a sheet product, `@[Name](item_image:UUID)` for one\nreal photo (this is how \"use the actual photo of our entrance\" is honoured), `asset:` for\nBrand Media. Say how to use it: \"as the background, exactly as-is\". Tag sparingly — the\nslide's own references and yours share one per-model budget, so tagging several can push the\nslide's existing subjects out of the frame.\n\nOn a **dynamic** deck `text` is refused — the words are a layer over the photo, not part of\nit, so rewriting them through the image model would leave the slide with two headlines.\n`instruction` works normally there and is usually all you need: it replaces the picture and\nthe existing words re-fit themselves to it. To change the *words*, use `set_cover_text` with\nthe `carousel_id` and `slide_number` — free, instant, no new picture.\n\n### Saving the user's photos to Brand Media — always describe them yourself\n\n`upload_asset` (a URL) and `get_upload_token` (local files via curl) put the user's own photos into\nBrand Media. You have usually SEEN the photo, so pass **`visual_description`**: 2-4 factual\nsentences, and say FIRST if the photo carries any text, logo or signage. Then it costs the user\nnothing. Without it, a workspace that uses its own photos as covers pays for Beevi to look at the\nphoto (about 1 credit per 70 photos) and the tool result carries a `note` saying so. With curl, put\neach `-F visual_description=…` immediately after its `-F file=@…`. To describe a photo that is\nalready uploaded, `update_asset({ asset_id, visual_description })`.\n\n### \"Put these photos on the post\" (the user's OWN pictures) — `set_post_photos`\n\nWhen the user hands you photos — chat attachments, which arrive with an `asset_id`, or ids from\n`list_assets` — and wants them published, that is `set_post_photos`. **Free and instant.** It\ndoes not generate, edit or restyle anything; the photos go up exactly as supplied. This is the\nanswer to \"make a carousel out of these\", \"post these five shots\", \"use my photos instead\".\n\n```\nset_post_photos({ post_id | social_post_ids, photo_asset_ids: [...] })\n```\n\n- **The order you pass IS the slide order.** Nothing re-sorts them.\n- Target `post_id` for every photo-capable platform of the post, or `social_post_ids` for\n specific platform versions. Exactly one of the two — not both, not neither. All the ids must\n belong to one post.\n- **REPLACE, not append.** It swaps out whatever media those posts have. To *add* photos, call\n `get_post` first and pass the existing ones plus the new ones.\n- **Photos only.** A post holds photos OR one video, never both — a video in the set is refused.\n To set a video cover, use `update_social_post`'s `cover_media`.\n- Each platform version is trimmed to its own limit (x 4, google_business 1, facebook/instagram\n 10, linkedin 20, tiktok 35) and the result reports `trimmed_from`. Published posts and\n platforms that can't take photos come back in `skipped[]` with a reason. **Tell the user in\n one line what was trimmed or skipped** — the card doesn't show it.\n- Editing a post this way **re-opens its approval** if it was scheduled.\n\n### Changing the words ON a cover — `get_cover_text` / `set_cover_text`\n\nA **dynamic** cover carries its headline as real text over the photo, so rewording it is a\ndatabase write: free, instant, no image regeneration, no credits. An **embedded** cover has\nthe words painted into the pixels and nothing can edit them in place.\n\n`get_cover_text({ post_id | social_post_id | carousel_id, slide_number? })` **is the test for\nwhich one you have** — call it first on any \"change what the cover says\" request:\n\n- `text_nodes` present → editable. Reword with `set_cover_text`.\n- `text_nodes` **empty** → baked into the photo. The only way to change them is to\n regenerate the picture (`update_post({ generate_cover_prompt })` for a post cover,\n `edit_carousel_slide({ slide_number, text })` for a deck slide). Both cost credits and\n produce a *different* picture — say so and get a go-ahead first.\n\n`set_cover_text` takes either `find` + `replace` (prefer it when the user quoted the wording\n— it cannot rewrite the words around the ones they asked about) or `edits: [{node_id, text}]`\naddressed by ids `get_cover_text` returned. **Never invent a node id**, and note it can only\nreword lines that already exist — adding a new subheadline is an editor job. Defaults to\nevery platform of a post / every slide of a deck; narrow with `social_post_id` or\n`slide_number`. Refused on published posts and video covers.\n\n**Order matters:** edit the deck *before* `attach_carousel`. Attaching copies the slides\ninto the post, and later slide edits don't flow through to a post already attached.\n\n### The first comment under a post — `get_auto_comments` / `set_auto_comments`\n\nBeevi can publish a comment under its own post the moment that post goes live — the\n\"link in first comment\" play. `set_auto_comments({ post_id | social_post_id, comments:\n[{content, delay_minutes}] })` **REPLACES the whole list**, so call `get_auto_comments`\nfirst when editing one of several; use `mode: 'append'` to add one without touching the\nrest. No comment id ever crosses the wire, which is deliberate — there is none to invent.\n\nWrite each platform in its own idiom, and note these are platform facts, not house style:\na real clickable URL on LinkedIn, Facebook and X (keep X under 280 characters); on\nInstagram a URL in a comment is dead text, so point at the bio instead. TikTok, YouTube and\nGoogle Business cannot take one at all, and Facebook/Instagram are gated off in production\npending Meta's review — the tool says which when it refuses.\n\n**Only before the post publishes.** The comment's timer starts at the real publish moment,\nso a post that is already live can no longer take one and the tool will refuse. Schedule\nthe comment while you schedule the post. `delay_minutes: 0` is right for a link — it should\nbe there when readers arrive.\n\n### \"Generate a campaign about our X\" (menu, product line, event series)\n1. Resolve X — a campaign needs *enough material*: several memories/items to spread\n across posts. List what you found and how it maps to a sequence.\n2. Gate: duration, cadence/post count, platforms, content types (text / +carousel /\n +video) — these are exactly the ambiguities worth one bundled question.\n3. `create_campaign`. Report the plan, don't just say \"done\". Two modes: for an open\n ask, use `brief` mode (put the cadence wording in `brief`, compute `post_count`\n yourself) so the campaign agent plans it; if the plan was already shaped in chat,\n pass pre-planned `posts[]` instead for an instant, no-planning-credit create.\n\n### \"Rename / pause / delete the campaign\" · \"move this post into the campaign\"\n- `manage_campaign` action `update`: title, description, period, and pause/resume\n (`status` — only active↔paused transitions are allowed).\n- `manage_campaign` action `delete` is PERMANENT — confirm with the user first, and\n state what happens to its posts: `delete_posts=false` (default) keeps them detached;\n `=true` also deletes them and is refused if any is already published.\n- Move a post in/out of a campaign: `update_post` with `campaign_id` (a campaign id\n attaches, `null` detaches). Campaign dates do NOT auto-adjust to the moved post.\n- Delete a whole post (all platforms): `delete_post` — permanent, confirm first;\n refused if any platform is already published (use `delete_social_post` for the\n unpublished platforms instead).\n\n### \"Help me think of what to post\" (ideation — reads only, free)\n1. Pull broadly: brand profile, memories, collections, recent posts (to avoid\n repeating), upcoming season/dates.\n2. Offer ~5 concrete, brand-specific angles, each naming its grounding (\"Your 'Top 10\n rooftop bars' award memory → a 'golden hour at Top of the Gate' reel\").\n3. Generate nothing until the user picks.\n\n### \"Post ideas from formats\" (inspiration — reads only until the user picks)\nWhen the user asks for post ideas or inspiration, call `list_recipes` (proven post formats for\ntheir niche). Show a few and let them pick. Before generating, ask up to 3 short questions about\nwhat the chosen format needs (their product, a photo, a detail), and say the AI cover costs\ncredits. Then `generate_post` with that format's `recipe_id` and their answers in `prompt`.\n\n### \"Does this sound like us?\" / \"Make it more our voice\"\n`get_brand_profile` → check against voice_description, tone, banned phrases — quote\nwhich rule the draft violates. Rewriting copy text is free (it's your text edit);\nregenerating a post via tools is not.\n\n### \"Schedule / publish\"\n- Scheduling = `update_post` with a time (respect the business's timezone).\n- Scheduling to a platform the workspace has not connected (or whose token needs a\n reconnect) still succeeds, but the result carries `unconnected_platforms` + a `warning`\n — relay it verbatim: the publish will fail unless the user connects it in Integrations\n before the slot. `get_workspace.connected_platforms` tells you up front.\n- `publish_post` posts publicly and irreversibly → echo exactly what will go out,\n where, and when, and get an explicit yes first. Same for `approve_post` when\n approvals are on: approving is a decision that belongs to the user.\n- Once the org's due posts have scheduled generation working, the publisher picks up a\n `scheduled` post within ~15s of its slot — but only once the publisher process is live\n (`SCHEDULED_PUBLISH_MODE=publisher`). Until then the every-minute cron publishes within\n ~1 min, so a delay of a minute or two is normal; \"why hasn't it published yet\" a minute\n after the slot is a real problem once the publisher is live, not before. A post you\n reschedule or unschedule after the publisher has already picked it up for this tick is\n simply skipped by that tick, not double-published — the new time takes effect on the\n next sweep.\n- Auto-generation for an org that's out of credits pauses for up to an hour (not forever) and\n resumes immediately the moment credits change (top-up, plan change, trial extension) — so\n \"buy credits\" is a real fix, not just advice, if that's what `generation_failed` turns out\n to be.\n\n### \"Remember that we never say X\" / brand corrections\nDurable facts → `create_memory`. Actual identity changes (voice, audience, banned\nphrases) → the `update_brand_*` tools — but read `get_brand_profile` first and change\nonly the field in question; these tools overwrite.\n\n## Gotchas (each one is a real observed failure)\n\n| Trap | Rule |\n|---|---|\n| Empty \"Characters\" collection | A collection name is not evidence; list its items and check `ready` status before tagging or promising. |\n| Wrong language | The brand's `language` field governs output language. Hebrew brand → Hebrew post, RTL-safe. Never default to English. |\n| Generic content | If you didn't cite a memory/item/post in your grounding line, you probably invented something. Go back to Resolve. |\n| No visuals exist | Many brands have zero collections. Say so and proceed with brand identity + logo — don't fake a reference or stall. |\n| Gallery item in post text | Gallery items are for image/video/cover generation only; post-body and carousel generation reject them. |\n| Tagging a gallery *collection* | Never taggable — tag its individual items. |\n| Reference overflow | Collection/gallery tags spread into many images against a ~14-image model cap. Tag the few that matter. |\n| Post idea vs social post | \"The post failed\" usually means ONE platform variant failed; check per-platform statuses in `get_post` before regenerating everything. |\n| Publish ≠ schedule | Publishing is public and immediate. Never publish when asked to schedule; never publish without an explicit confirmation. |\n| A hidden owner or place | `owner: hidden` or `place: hidden` means covers will never show them — don't tag that person or a `My business` location, and don't promise a photo of them. Say the rule is on and offer to change it with `update_photo_rules` if they ask. |\n| Overwriting brand identity | `update_brand_*` tools replace fields. Read-modify-write, never write blind. |\n"Full snapshot data
{
"description": "Fundamentals of Beevi for agents driving it through the Beevi MCP tools. Beevi is an AI branding agency for SMBs — it learns a business's brand and creates, schedules, and publishes social content (posts, campaigns, cover images, carousels, videos) across LinkedIn (personal profile and Company Page), X, Instagram, Facebook, TikTok, YouTube, Google Business Profile. Use this skill whenever a request involves a business's social content or brand: creating or scheduling posts, generating cover images or other visuals, planning campaigns, brainstorming post ideas, using or improving the brand identity/voice, or working with the business's visual collections (products, characters, properties). Trigger even when Beevi isn't named — e.g. \"make a post about our main dish\", \"generate a cover for the jacuzzi post\", \"plan a campaign about our menu\", \"help me think of something to post\".\n",
"included_files": [],
"name": "beevi-expert",
"skill_md_contents": "---\nname: beevi-expert\ndescription: >\n Fundamentals of Beevi for agents driving it through the Beevi MCP tools. Beevi is an\n AI branding agency for SMBs — it learns a business's brand and creates, schedules, and\n publishes social content (posts, campaigns, cover images, carousels, videos) across\n LinkedIn (personal profile and Company Page), X, Instagram, Facebook, TikTok, YouTube,\n Google Business Profile. Use this skill whenever a request\n involves a business's social content or brand: creating or scheduling posts, generating\n cover images or other visuals, planning campaigns, brainstorming post ideas, using or\n improving the brand identity/voice, or working with the business's visual collections\n (products, characters, properties). Trigger even when Beevi isn't named — e.g. \"make a\n post about our main dish\", \"generate a cover for the jacuzzi post\", \"plan a campaign\n about our menu\", \"help me think of something to post\".\n---\n\n# Beevi Expert — operating a brand through Beevi\n\nBeevi is a SaaS branding agency for small businesses. The business's brand identity,\nknowledge, and visual references are **already in Beevi** — your job is never to invent\ncontent from thin air, but to *retrieve what the brand already knows about itself* and\ngenerate from that. An agent that skips retrieval produces generic, off-brand content\nand wastes the user's money.\n\n## The one economic rule\n\n**Reads are free — spend them lavishly. Writes cost the user credits — never spend one\non an unresolved ambiguity.**\n\nEvery `list_*` / `get_*` tool is free. Every `generate_*` / `create_*` / `publish_*`\ntool costs real credits and/or posts publicly. So: search first, ground everything,\nask when genuinely ambiguous, and only then generate. An agent that internalizes this\nrule derives the rest of this skill on its own.\n\n## The object model (what everything is)\n\n### Know which workspace you're in (do this first)\n\nYour API key is scoped one of two ways, and it changes your first move:\n\n- **Workspace-scoped** — every tool call operates on that one workspace automatically;\n `workspace_id` args are ignored. `list_workspaces` returns exactly one entry.\n- **Organization-scoped** — always the case when you connected through a directory\n (Claude's or ChatGPT's \"connect\" button runs OAuth, which mints an organization key). You\n can reach several workspaces and must pass\n `workspace_id` explicitly. If the user's request doesn't unambiguously name which\n brand (or `list_workspaces` returns more than one), **confirm the workspace with the\n user before doing anything else** — every read you make and every credit you spend\n lands in a specific brand's account, and operating on the wrong client's brand is the\n worst mistake available to you.\n\nEither way, call `list_workspaces` once at the start so you know whose brand you're\nspeaking for — don't infer it from the user's phrasing.\n\n| Object | What it is | Read with |\n|---|---|---|\n| **Workspace / business** | One brand. All tools are scoped to a workspace (business). | `list_workspaces`, `get_workspace` |\n| **Brand profile** | The brand's identity: description, voice, tone, preferred/banned phrases, colors, fonts, target audience, competitors, goals, cover-image guidelines, **language**. | `get_brand_profile` |\n| **Photo rules** (\"What Beevi can show\") | The owner's standing rules for what a COVER may show: `place` (real / imagine / hidden — their own space, the `location` items tagged `My business`), `owner` (ai_scenes / real_only / hidden — their face), `product_style`, `work` (real / hidden), `ai_people` (yes / no). Absent = never chosen. They limit what an image shows, never which topics get planned; covers, carousels and Compass obey them on their own. Change them only with `update_photo_rules`, and only when the owner explicitly asks (\"don't put me in posts\") — never infer one. | `photoRules` in `get_brand_profile` |\n| **Memory** | A stored fact the brand wants recalled when generating — menu items, pricing, awards, taglines, policies. The richest source for \"post about our X\". | `list_memories`, `get_memory` |\n| **Weekly Compass** | The brand's weekly \"what to consider this week\" signals: rising `search_trends` (each with a `post_angle`), recent `relevant_content` articles, `calendar_events`, and a markdown `overview` digest. The best source for \"what should I post about right now?\" / trend-driven ideas. Read-only; may be empty if the brand hasn't opted in. | `get_compass` |\n| **Performance** | How published posts actually did, per platform (views, engagements, engagement rate, followers, best times to post). `get_analytics_summary` is the overview: `status` + `next_step` when there are no numbers (`connect`, `reconnect`, `unavailable` or `waiting` — say the next step), a `summary` per metric with `change` / `change_kind`, top and lowest posts, `best_times` and `unavailable` notes (add `include_daily: true` only if you need the daily series). `list_post_performance` ranks posts (`sort`: `usual`, `usual_asc`, `views`, `engagements`, `engagement_rate`, `date`); `vs_usual` 1.0 = a typical post for that account, above 1 beat it. `vs_usual` and `change` can also be `null` when there is not enough history to compare. `get_post_performance` takes a `published_post_id` or a `post_id` and returns the numbers plus the growth curve (with a `post_id`, platforms without numbers come back in `missing`: `not_synced_yet` or `not_tracked`). `last_synced_at: null` (or a \"Not synced yet\" note in `unavailable`) means the first sync is still pending — numbers arrive within a few hours of publishing. On a synced post a `null` metric means the platform did not report it, never 0 — do not average it in or say \"no engagement\". Call `get_analytics_summary` before recommending what to post more of; use `list_post_performance` with `sort: usual` for the best posts. Free on every plan; `get_post` / `list_posts` (with `include_content`) also carry a `performance` block. | `get_analytics_summary`, `list_post_performance`, `get_post_performance` |\n| **Post idea** | One unit of content as the user thinks of it: prompt, cover image, schedule time, status. | `list_posts`, `get_post` |\n| **Social post** | A per-platform variant of a post idea (LinkedIn copy ≠ Instagram copy). Each has its own status: drafted / pending_approval / approved / scheduled / sent / failed. | inside `get_post` |\n| **Campaign** | A themed sequence of posts over a duration (e.g. \"2-week menu campaign\" → ~8 posts). | `list_campaigns`, `get_campaign` |\n| **Collection** (Visuals) | A named group of visual reference items: products, characters, properties, locations. Comes in two kinds, and every item carries free-text `tags` — see below. | `list_collections`, `list_collection_items`, `get_collection_item` |\n| **Asset** (Brand Media) | A Brand Media file. `origin` says who put it there: `user` (the customer) or `generated` (a model). `list_assets` filters on it, and on `unused` (never in a post yet — entries that have been carry a `used` tag; `get_asset` prints **Last used**). | `list_assets`, `get_asset` |\n| **Style** | A saved visual style (e.g. \"Watercolor\") referenced by name+description in prompts. | `list_styles` |\n| **Visual template** | A reusable cover *overlay* (text/logo/badges) — the business's saved templates + built-in starter presets. Apply one onto a post cover with `apply_visual_template`. | `list_visual_templates` |\n| **Carousel** | A multi-slide image post for LinkedIn/Instagram. You can let the AI plan the deck, or write each slide yourself. | `list_carousels`, `get_carousel`, `generate_carousel`, `edit_carousel_slide`, `attach_carousel` |\n| **Recipe** (post format) | A proven post format for the brand's niche (idea, angle, cover look; carousel recipes also carry a slide arc). Owners browse them on the Inspiration page and Compass plans from them. `list_recipes` lists them (8 per page, `niche` for another kind of business); `generate_post` with a `recipe_id` makes a post in one, always with an AI cover (costs credits). See \"Post ideas from formats\" below. | `list_recipes`, `generate_post` (`recipe_id`) |\n| **Job** | Long-running generation runs asynchronously; poll with `get_jobs`. | `get_jobs` |\n\n### The two kinds of collection (the key Visuals distinction)\n\n- **Reference sheet** (`reference_style: sheet`) — each item is ONE identity-locked\n image of a character or product. Tagging it keeps that subject looking identical\n across generations. An item is usable **only when its sheet status is `ready`**.\n- **Gallery** (`reference_style: multi_image`) — each item is a BUNDLE of real photos\n of one thing (a property, a store). Tagging the item spreads ALL its photos in as\n references; you can also tag one specific photo.\n\n**A collection's name is not evidence. Only a listed, ready item is.** Businesses often\nhave an empty \"Characters\" collection — always `list_collection_items` and check status\nbefore promising or tagging anything.\n\n### Item tags\n\nEvery item carries up to 10 free-text `tags`, returned by both `list_collection_items` and\n`get_collection_item`. They are labels you can reason about — the suggested vocabulary per\ncollection type is spelled out in those tools' own descriptions.\n\n- **`My business`** is the one tag code keys on: it marks the location item that IS the\n business's own premises (written during onboarding, when the owner is asked \"which of these\n is your business?\"). That is how you find \"our shop\" / \"our office\" for a post — read the tag,\n don't guess from item names.\n- `manage_collection_item` writes them **replace-not-merge**, same as `metadata` — call\n `get_collection_item` first and send back the full list, or you will silently drop tags.\n\n## @-mentions: how you wire brand material into generation\n\nPrompts passed to generation tools support `@[Name](scheme:uuid)` mentions. This is how\nretrieved material actually reaches the model — a mention is not decoration, it attaches\nthe real reference:\n\n| Mention | Attaches | Use for |\n|---|---|---|\n| `@[Maya](item:uuid)` | The item's reference sheet (1 image) or, for a gallery item, ALL its photos | Consistent characters/products; real property/location photos, app screenshots |\n| `@[Photo 3](item_image:uuid)` | One specific gallery photo | When only one photo of a place is wanted |\n| `@[Products](collection:uuid)` | Every ready sheet in the collection | \"Show our whole product line\" — sheet collections only, never gallery collections |\n| `@[Brand Logo](asset:00000000-0000-0000-0000-000000000001)` | The brand logo image | Putting the logo in a generated image |\n| `@[Photo](asset:uuid)` | That asset (either origin) | Any Brand Media file as visual reference |\n| `@[Watercolor](style:uuid)` | **Text only** (name + description, no images) | Visual style direction |\n| `@[Pricing](memory:uuid)` | **Text only** (the fact, inlined) | Grounding copy in a stored fact |\n\nSurface limits worth knowing. A whole gallery item attaches **every photo it has**, so where\nit may go depends on how many references that surface can hold:\n\n- **image / cover / video** — fine, it spreads.\n- **carousel deck `prompt`** — fine, and it is the good move: the item becomes a *catalogue*\n and the planner puts the photo that fits onto each slide. This is how you say \"use our real\n photos\".\n- **a single carousel slide, or `edit_carousel_slide`** — refused (every photo onto one image).\n Tag ONE photo there with `@[Name](item_image:UUID)` from `get_collection_item`.\n- **post body / campaign text** — refused; that text is written from words, not pictures.\n\n`item_image:` is one reference, so it is welcome anywhere an image is rendered. Image models\ncap total references (~14) — tag the few that matter.\n\n## The loop: Resolve → Ground → Gate → Execute\n\nRun this loop for every content request. The common failure mode is skipping straight\nto Execute.\n\n### 1. Resolve the subject (free)\n\n\"The jacuzzi\", \"our main dish\", \"the new sunscreen\" — the subject almost always already\nexists somewhere in Beevi. Search, in parallel:\n\n- `list_memories` — facts (\"Joe's is known for 18-inch NY-style pies\")\n- `list_collections` + `list_collection_items` — visual references (a \"Radiance Serum\"\n product sheet, a property gallery)\n- `list_posts` — existing posts (\"the post about the jacuzzi\" → find that actual post). Also\n filterable by `lifecycle` — `needs_attention` (awaiting approval / overdue / declined / failed\n generation / partially published — a platform leg failed), `upcoming` (scheduled), `drafts` (draft or\n still generating), `published` (fully published) — to answer \"what needs my attention\" or \"what's\n going out next\" in one call instead of scanning everything. A post whose scheduled time passed\n without ever generating (e.g. the org ran out of credits) shows up here as `generation_failed`\n with `publish_error`: *\"Not generated before its scheduled time (e.g. out of credits). Open it and\n click Generate.\"* — explain that plainly and offer to `generate_post` it now, or point at a top-up\n if credits are the actual cause.\n- `list_assets` — the customer's photos (`origin: 'user'`) or model output (`origin: 'generated'`); `unused: true` narrows to photos that have never been on a post — the pool to pick a fresh cover reference from, so the same product shot is not reused week after week (a photo counts as used once a post carries it, or carries an AI image made from it)\n\nSearch results are shallow — **open the top candidates with `get_post` /\n`get_collection_item` before concluding something doesn't exist.** The subject often\nlives inside a platform variant's body text, not in the title or search index, and a\n\"no such post exists\" verdict you didn't verify is worse than a wrong guess: it sends\nthe user to re-explain something you already had.\n\nWhile you're in `list_posts`, also check for **existing content about the same\nsubject** — an unsent draft about the same product means your job may be to revive or\nupdate it, not to pay for a duplicate.\n\nThe subject may live in one, several, or none of these. Multiple plausible matches\n(three sunscreen products) → that's a question for the user, not a guess.\n\n### 2. Ground in the brand (free, never skip)\n\nCall `get_brand_profile` before generating anything. (Generation tools do inject brand\ncontext server-side, but that doesn't replace reading the profile yourself — you need\nit to write the prompt in the right language, honor banned phrases, and know the cover\nrules *before* you spend, not discover them in the output.) Apply:\n\n- **Language** — generate in the brand's language. A Hebrew brand gets Hebrew posts.\n The voice_description may itself be written in that language — that IS the voice;\n don't translate it away or default to English.\n- **Voice & tone** — write like the brand, not like an assistant.\n- **Banned/preferred phrases** — hard constraints on copy.\n- **Cover guidelines, colors, logo position** — hard constraints on cover images.\n- **Target audience & goals** — pick angles that serve them.\n\n### 3. Gate: the pre-spend checklist (this is where you ask)\n\nBefore ANY credit-spending call, you must be able to name all three:\n\n1. **The resolved subject, with evidence** — a specific memory / item / post / asset ID,\n or an explicit \"nothing found — proceeding on brand identity plus the user's words\".\n2. **The target** — which platforms, or which existing post the cover is for.\n3. **The visual grounding** — which mentions you'll attach, or an explicit\n \"no relevant visuals exist\".\n\nAny of the three missing or ambiguous → ask the user. Bundle everything into **one\nmessage with at most ~2 questions**; offer the concrete options you found (\"I found\nthree sunscreens: PROTECT ME, PROTECT BODY, GLOW ME+ — which one?\"). If all three are\nresolved, do NOT ask permission to proceed — act.\n\n**Show your grounding** as you proceed, in one line: *\"Found the jacuzzi in the\nrooftop-spa memory and 4 photos in the 'Amenities' gallery — using those.\"* This is the\ncheapest way for the user to catch a wrong resolution before credits burn.\n\n### 4. Execute (costs credits)\n\nPick the narrowest tool that does the job, wire in the mentions, then poll `get_jobs`\nif the run is async, and report what was created (with links/IDs).\n\n## Per-intent playbooks\n\n### \"Generate a cover image for the post about X\"\n1. `list_posts` → find the actual post about X (it usually exists — that's why they said \"the post\").\n2. Resolve X's visuals: gallery photos of the real thing beat AI imagination — a real\n jacuzzi photo tagged as reference makes a truthful cover.\n3. `get_brand_profile` → cover guidelines, colors, logo position.\n4. Gate, then `generate_cover_image` for that post with the mentions attached.\n5. If the post **already has a cover**, replacing it is a destructive-ish edit — say\n you're replacing it (or ask, if the user only said \"make a cover\" without naming\n the post confidently).\n\n### \"Create a post about our X\" (main dish, new product, a property…)\n1. Resolve X across memories / collections / assets. The best post copy is grounded in\n a real fact (\"late-night weekend hours\", \"$340/year\") — tag the memory.\n2. `get_brand_profile` → language + voice.\n3. Gate (platforms? one product or several matches?).\n4. `generate_post` (full agent run: copy + platform variants + cover) or `create_post`\n (you write the content). Prefer `generate_post` unless the user dictated exact copy.\n To make it a **carousel**, pass `post_type: 'carousel'` — the one post job also generates\n and attaches a branded multi-slide carousel on carousel-capable platforms (costs extra\n carousel credits; falls back to a plain post if the platforms can't host one).\n\n### \"Make a carousel with these slides\" (the user dictates the content)\n**First check whose pictures these are.** If the user handed you actual photos, they want those\nphotos posted — see the next playbook. `generate_carousel` *invents* artwork and costs credits;\nreach for it only when new pictures are what's wanted.\n\n`generate_carousel` has two modes. Given only a topic, an AI planner writes the deck.\nBut when the user tells you what the slides should say — or you have researched the\nsubject and know — pass `slides[]` and write them yourself; the planner is skipped and\ntheir wording survives verbatim.\n\nEach slide is `{ text, visual }`:\n- `text` — every word to render ON the slide, in quotes, with placement where it\n matters: `\"Best in town\" large in the middle, \"Guaranteed\" small bottom-left`. Write\n the words, never a label for them (\"Headline: …\" renders the word \"Headline\").\n- `visual` — what the slide SHOWS, subject only. Don't put palette or fonts here; those\n come from `visual_style` (deck-level, optional — omit it and the brand profile\n supplies one). Per-slide styling is how a deck ends up looking like three decks.\n- Tag a product with `@[Name](item:UUID)` inside `visual` to put its real photo on that\n slide. Sheet items only.\n\n**Where the words live — `text_mode`.** By default the image model paints the words into\neach picture. Pass `text_mode: 'dynamic'` and it shoots the photographs text-free instead,\nthen lays the copy over them as real text in the brand font: crisp type at any size, no\nmisspelled words, and the user can retype or move it afterwards in the Carousel Maker. Good\nfor a wordy deck, a brand that cares about its typography, or any language the image model\nspells badly. It cannot be combined with `slides[]` or `template_id` — a dynamic deck's copy\nis written by the planner alongside the photographs it has to fit, and a template already\ndefines where text sits. Pass `prompt` alone.\n\nTo change a deck afterwards, `edit_carousel_slide` handles one slide: `instruction`\nfor the look (\"make it darker\"), `text` for new words, or both. Changing several slides\nmeans several calls — in the in-app chat they run in parallel when emitted in one turn.\nAn edit keeps the references the slide already has, and a mention in `instruction` attaches\nits photo too — `@[Name](item:UUID)` for a sheet product, `@[Name](item_image:UUID)` for one\nreal photo (this is how \"use the actual photo of our entrance\" is honoured), `asset:` for\nBrand Media. Say how to use it: \"as the background, exactly as-is\". Tag sparingly — the\nslide's own references and yours share one per-model budget, so tagging several can push the\nslide's existing subjects out of the frame.\n\nOn a **dynamic** deck `text` is refused — the words are a layer over the photo, not part of\nit, so rewriting them through the image model would leave the slide with two headlines.\n`instruction` works normally there and is usually all you need: it replaces the picture and\nthe existing words re-fit themselves to it. To change the *words*, use `set_cover_text` with\nthe `carousel_id` and `slide_number` — free, instant, no new picture.\n\n### Saving the user's photos to Brand Media — always describe them yourself\n\n`upload_asset` (a URL) and `get_upload_token` (local files via curl) put the user's own photos into\nBrand Media. You have usually SEEN the photo, so pass **`visual_description`**: 2-4 factual\nsentences, and say FIRST if the photo carries any text, logo or signage. Then it costs the user\nnothing. Without it, a workspace that uses its own photos as covers pays for Beevi to look at the\nphoto (about 1 credit per 70 photos) and the tool result carries a `note` saying so. With curl, put\neach `-F visual_description=…` immediately after its `-F file=@…`. To describe a photo that is\nalready uploaded, `update_asset({ asset_id, visual_description })`.\n\n### \"Put these photos on the post\" (the user's OWN pictures) — `set_post_photos`\n\nWhen the user hands you photos — chat attachments, which arrive with an `asset_id`, or ids from\n`list_assets` — and wants them published, that is `set_post_photos`. **Free and instant.** It\ndoes not generate, edit or restyle anything; the photos go up exactly as supplied. This is the\nanswer to \"make a carousel out of these\", \"post these five shots\", \"use my photos instead\".\n\n```\nset_post_photos({ post_id | social_post_ids, photo_asset_ids: [...] })\n```\n\n- **The order you pass IS the slide order.** Nothing re-sorts them.\n- Target `post_id` for every photo-capable platform of the post, or `social_post_ids` for\n specific platform versions. Exactly one of the two — not both, not neither. All the ids must\n belong to one post.\n- **REPLACE, not append.** It swaps out whatever media those posts have. To *add* photos, call\n `get_post` first and pass the existing ones plus the new ones.\n- **Photos only.** A post holds photos OR one video, never both — a video in the set is refused.\n To set a video cover, use `update_social_post`'s `cover_media`.\n- Each platform version is trimmed to its own limit (x 4, google_business 1, facebook/instagram\n 10, linkedin 20, tiktok 35) and the result reports `trimmed_from`. Published posts and\n platforms that can't take photos come back in `skipped[]` with a reason. **Tell the user in\n one line what was trimmed or skipped** — the card doesn't show it.\n- Editing a post this way **re-opens its approval** if it was scheduled.\n\n### Changing the words ON a cover — `get_cover_text` / `set_cover_text`\n\nA **dynamic** cover carries its headline as real text over the photo, so rewording it is a\ndatabase write: free, instant, no image regeneration, no credits. An **embedded** cover has\nthe words painted into the pixels and nothing can edit them in place.\n\n`get_cover_text({ post_id | social_post_id | carousel_id, slide_number? })` **is the test for\nwhich one you have** — call it first on any \"change what the cover says\" request:\n\n- `text_nodes` present → editable. Reword with `set_cover_text`.\n- `text_nodes` **empty** → baked into the photo. The only way to change them is to\n regenerate the picture (`update_post({ generate_cover_prompt })` for a post cover,\n `edit_carousel_slide({ slide_number, text })` for a deck slide). Both cost credits and\n produce a *different* picture — say so and get a go-ahead first.\n\n`set_cover_text` takes either `find` + `replace` (prefer it when the user quoted the wording\n— it cannot rewrite the words around the ones they asked about) or `edits: [{node_id, text}]`\naddressed by ids `get_cover_text` returned. **Never invent a node id**, and note it can only\nreword lines that already exist — adding a new subheadline is an editor job. Defaults to\nevery platform of a post / every slide of a deck; narrow with `social_post_id` or\n`slide_number`. Refused on published posts and video covers.\n\n**Order matters:** edit the deck *before* `attach_carousel`. Attaching copies the slides\ninto the post, and later slide edits don't flow through to a post already attached.\n\n### The first comment under a post — `get_auto_comments` / `set_auto_comments`\n\nBeevi can publish a comment under its own post the moment that post goes live — the\n\"link in first comment\" play. `set_auto_comments({ post_id | social_post_id, comments:\n[{content, delay_minutes}] })` **REPLACES the whole list**, so call `get_auto_comments`\nfirst when editing one of several; use `mode: 'append'` to add one without touching the\nrest. No comment id ever crosses the wire, which is deliberate — there is none to invent.\n\nWrite each platform in its own idiom, and note these are platform facts, not house style:\na real clickable URL on LinkedIn, Facebook and X (keep X under 280 characters); on\nInstagram a URL in a comment is dead text, so point at the bio instead. TikTok, YouTube and\nGoogle Business cannot take one at all, and Facebook/Instagram are gated off in production\npending Meta's review — the tool says which when it refuses.\n\n**Only before the post publishes.** The comment's timer starts at the real publish moment,\nso a post that is already live can no longer take one and the tool will refuse. Schedule\nthe comment while you schedule the post. `delay_minutes: 0` is right for a link — it should\nbe there when readers arrive.\n\n### \"Generate a campaign about our X\" (menu, product line, event series)\n1. Resolve X — a campaign needs *enough material*: several memories/items to spread\n across posts. List what you found and how it maps to a sequence.\n2. Gate: duration, cadence/post count, platforms, content types (text / +carousel /\n +video) — these are exactly the ambiguities worth one bundled question.\n3. `create_campaign`. Report the plan, don't just say \"done\". Two modes: for an open\n ask, use `brief` mode (put the cadence wording in `brief`, compute `post_count`\n yourself) so the campaign agent plans it; if the plan was already shaped in chat,\n pass pre-planned `posts[]` instead for an instant, no-planning-credit create.\n\n### \"Rename / pause / delete the campaign\" · \"move this post into the campaign\"\n- `manage_campaign` action `update`: title, description, period, and pause/resume\n (`status` — only active↔paused transitions are allowed).\n- `manage_campaign` action `delete` is PERMANENT — confirm with the user first, and\n state what happens to its posts: `delete_posts=false` (default) keeps them detached;\n `=true` also deletes them and is refused if any is already published.\n- Move a post in/out of a campaign: `update_post` with `campaign_id` (a campaign id\n attaches, `null` detaches). Campaign dates do NOT auto-adjust to the moved post.\n- Delete a whole post (all platforms): `delete_post` — permanent, confirm first;\n refused if any platform is already published (use `delete_social_post` for the\n unpublished platforms instead).\n\n### \"Help me think of what to post\" (ideation — reads only, free)\n1. Pull broadly: brand profile, memories, collections, recent posts (to avoid\n repeating), upcoming season/dates.\n2. Offer ~5 concrete, brand-specific angles, each naming its grounding (\"Your 'Top 10\n rooftop bars' award memory → a 'golden hour at Top of the Gate' reel\").\n3. Generate nothing until the user picks.\n\n### \"Post ideas from formats\" (inspiration — reads only until the user picks)\nWhen the user asks for post ideas or inspiration, call `list_recipes` (proven post formats for\ntheir niche). Show a few and let them pick. Before generating, ask up to 3 short questions about\nwhat the chosen format needs (their product, a photo, a detail), and say the AI cover costs\ncredits. Then `generate_post` with that format's `recipe_id` and their answers in `prompt`.\n\n### \"Does this sound like us?\" / \"Make it more our voice\"\n`get_brand_profile` → check against voice_description, tone, banned phrases — quote\nwhich rule the draft violates. Rewriting copy text is free (it's your text edit);\nregenerating a post via tools is not.\n\n### \"Schedule / publish\"\n- Scheduling = `update_post` with a time (respect the business's timezone).\n- Scheduling to a platform the workspace has not connected (or whose token needs a\n reconnect) still succeeds, but the result carries `unconnected_platforms` + a `warning`\n — relay it verbatim: the publish will fail unless the user connects it in Integrations\n before the slot. `get_workspace.connected_platforms` tells you up front.\n- `publish_post` posts publicly and irreversibly → echo exactly what will go out,\n where, and when, and get an explicit yes first. Same for `approve_post` when\n approvals are on: approving is a decision that belongs to the user.\n- Once the org's due posts have scheduled generation working, the publisher picks up a\n `scheduled` post within ~15s of its slot — but only once the publisher process is live\n (`SCHEDULED_PUBLISH_MODE=publisher`). Until then the every-minute cron publishes within\n ~1 min, so a delay of a minute or two is normal; \"why hasn't it published yet\" a minute\n after the slot is a real problem once the publisher is live, not before. A post you\n reschedule or unschedule after the publisher has already picked it up for this tick is\n simply skipped by that tick, not double-published — the new time takes effect on the\n next sweep.\n- Auto-generation for an org that's out of credits pauses for up to an hour (not forever) and\n resumes immediately the moment credits change (top-up, plan change, trial extension) — so\n \"buy credits\" is a real fix, not just advice, if that's what `generation_failed` turns out\n to be.\n\n### \"Remember that we never say X\" / brand corrections\nDurable facts → `create_memory`. Actual identity changes (voice, audience, banned\nphrases) → the `update_brand_*` tools — but read `get_brand_profile` first and change\nonly the field in question; these tools overwrite.\n\n## Gotchas (each one is a real observed failure)\n\n| Trap | Rule |\n|---|---|\n| Empty \"Characters\" collection | A collection name is not evidence; list its items and check `ready` status before tagging or promising. |\n| Wrong language | The brand's `language` field governs output language. Hebrew brand → Hebrew post, RTL-safe. Never default to English. |\n| Generic content | If you didn't cite a memory/item/post in your grounding line, you probably invented something. Go back to Resolve. |\n| No visuals exist | Many brands have zero collections. Say so and proceed with brand identity + logo — don't fake a reference or stall. |\n| Gallery item in post text | Gallery items are for image/video/cover generation only; post-body and carousel generation reject them. |\n| Tagging a gallery *collection* | Never taggable — tag its individual items. |\n| Reference overflow | Collection/gallery tags spread into many images against a ~14-image model cap. Tag the few that matter. |\n| Post idea vs social post | \"The post failed\" usually means ONE platform variant failed; check per-platform statuses in `get_post` before regenerating everything. |\n| Publish ≠ schedule | Publishing is public and immediate. Never publish when asked to schedule; never publish without an explicit confirmation. |\n| A hidden owner or place | `owner: hidden` or `place: hidden` means covers will never show them — don't tag that person or a `My business` location, and don't promise a photo of them. Say the rule is on and offer to change it with `update_photo_rules` if they ask. |\n| Overwriting brand identity | `update_brand_*` tools replace fields. Read-modify-write, never write blind. |\n"
}SHA-256 of public snapshot: decd5296839c24bab2b1553dfb116c8f71f03c5105b95dd25712cf5c53a1ad88