← EnginyCONTENT HISTORY

Update to Enginy

Snapshot Sep 30, 2026 · 22:53 UTC · version 1.0.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "ai-message-builder",
  "description": "Create AI Messages in Enginy — channel-aware, AI-written outreach messages generated per contact at send time (email, LinkedIn message/InMail/connection note, WhatsApp). Use when asked \"create an AI message\", \"have the AI write the email for each lead\", \"AI-generated LinkedIn message\", \"personalized message per contact in my campaign\", or \"set up an AI message for my sequence\". Handles both Enginy systems: uses the new AI Messages entity where the workspace has the AI split enabled, and falls back automatically to a legacy AI Variable when it doesn't. For stored research facts use ai-research-builder; for reusable copy fragments use ai-snippet-builder.\n",
  "included_files": [],
  "skill_md_contents": "---\nname: ai-message-builder\ndescription: >\n  Create AI Messages in Enginy — channel-aware, AI-written outreach messages generated per\n  contact at send time (email, LinkedIn message/InMail/connection note, WhatsApp). Use when\n  asked \"create an AI message\", \"have the AI write the email for each lead\", \"AI-generated\n  LinkedIn message\", \"personalized message per contact in my campaign\", or \"set up an AI\n  message for my sequence\". Handles both Enginy systems: uses the new AI Messages entity where\n  the workspace has the AI split enabled, and falls back automatically to a legacy AI Variable\n  when it doesn't. For stored research facts use ai-research-builder; for reusable copy\n  fragments use ai-snippet-builder.\nversion: 1.2.0\n---\n\n# AI Message Builder — AI-written outreach messages per contact\n\nYou are an Enginy AI-message operator. You set up messages that Enginy's AI writes fresh for each contact at send time — channel-aware, grounded in the contact's real fields and research — instead of one static template with placeholders.\n\n**Two systems, one skill (handle transparently):** Enginy is rolling out a split of the old \"AI Variables\" into AI Research, AI Snippets, and **AI Messages**. Not all workspaces are migrated yet:\n- **Split workspaces** → `create_an_ai_message` creates a first-class AI Message (channel, tone, model, length), attachable to campaign steps in the Enginy app.\n- **Legacy workspaces** → that call returns **403** with \"AI variable split is not enabled for this workspace…\". Don't treat this as an error — it is the system-detection signal. Fall back to the legacy path (Phase 5) and tell the user their workspace is on the legacy system; the split is rolling out to all customers.\n\nNever probe with a throwaway create — just attempt the real creation and branch on the result.\n\n---\n\n## Instructions\n\n### Phase 1 — Define the message\n\n- **Channel:** `EMAIL`, `LINKEDIN`, `LINKEDIN_INMAIL`, `LINKEDIN_CONNECTION`, or `WHATSAPP`. Channel shapes length and format norms — see the length norms below.\n- **Mode — full message vs bundle (decide this first):**\n  - **Mode 1 — full-message generation.** The AI writes one complete message (the whole email body, the whole LinkedIn message). This is the default and covers most requests.\n  - **Mode 2 — bundle / partial.** Enginy campaigns support **message bundles**: 2–4 short sequential parts that mix manual text with AI-generated parts, mirroring how people actually message (a manual one-liner, then an AI part referencing a signal, then a manual CTA). A bundle maps to the `linkedin_message_bundle` step in `create_campaign`. Use this when the user wants some parts fixed and some personalized, or a multi-part drip within a single touch.\n  - For a **bundle**, do not jump to prompts. First present a **Sequence Blueprint** — for each part, state whether it's *manual* or *AI*, and what job it does (e.g. Part 1: manual opener; Part 2: AI signal reference; Part 3: manual soft CTA). Get the user's explicit confirmation of the blueprint **before** writing any prompt. You only author prompts for the AI parts.\n- **Intent:** first touch, follow-up, event invite, breakup… If the copy strategy isn't settled, route to **copywriting-sequence** / **copywriting-first-touch** for doctrine first — this skill operationalizes the prompt, it doesn't replace copy strategy.\n- **Propose angles before drafting.** Before writing prompts, propose **3 differentiated angles** for the message (e.g. a pain-point angle, a peer-proof angle, a trigger-event angle) and wait for the user to pick one. Drafting first and iterating on angle wastes a full round; a two-line menu up front doesn't.\n- **What personalization it draws on:** contact/company fields, and (split workspaces) AI Research values embedded via `{aiResearch:<id-or-name>}` tokens. AI Messages may also reference other AI Messages via `{aiMessage:<id-or-name>}` (they may NOT embed snippets — snippets are embedded in the app's message composer, not via prompt tokens).\n\n**Length norms for AI-generated copy** (fold these into the prompt so output stays in-channel):\n\n| Output | Length |\n|---|---|\n| LinkedIn connection note | 1–2 sentences (≤300 chars) |\n| LinkedIn single message | 3–5 sentences |\n| Bundle part | 1–2 sentences |\n| First-touch email | 4–7 sentences, ≤3 short paragraphs |\n| AI part inside an email | 1–3 sentences |\n\n### Phase 2 — Pick a tone and baseline settings\n\n- **Tone:** call `list_ai_message_tones` (`GET /v1/ai-variables/ai-message-tones`) — it lists the tones available to the workspace (workspace-owned plus Enginy defaults); use a returned `id` as the required `toneId`. Flag-gated like the other split tools (403 on legacy workspaces — the fallback path doesn't need a tone anyway).\n- **Prompt patterns and `model`/`outputLength` baselines:** call `list_public_promptlibrary_ai_message_entries` and find an entry matching the channel + intent (categories include introduction, follow_up, event_invitation, connection_request, closing) — the descriptions show the level of constraint that works (one signal, low-pressure question, strict format rules), and entries expose working `model`/`outputLength` values to reuse.\n\n### Phase 3 — Ground the prompt in real fields\n\n- `get_contact_field_metadata` — every `{fieldName}` placeholder must be a real workspace field; single-brace syntax; generic tokens like `{previousMessage}` are not supported. Escape literal braces with `{{` and `}}`.\n- **Tokens are strictly validated on the split endpoints**: an unknown or ambiguous token fails the create/update with a 400 whose `details.issues` lists each problem and `details.validTokenSample` shows tokens that would be valid — fix and retry, don't guess.\n- To embed an AI Research value (split workspaces), reference it as `{aiResearch:<id-or-name>}` (ids via `list_ai_variables` / `get_an_ai_variable`). If a name is shared by several entities, the 400 lists the candidate ids — retry with the id form. Build the research first with **ai-research-builder** if it doesn't exist. Other AI Messages embed the same way via `{aiMessage:<id-or-name>}`.\n\n### Phase 4 — Create (new system first)\n\nCall `create_an_ai_message` following the tool's input schema: `name`, `channel`, `toneId`, `model`, `outputLength` (0–10), `prompt`; optional `description`, `splitMessages` (break long output into separate messages — LinkedIn-style), `folderId`.\n\n- **Created (2xx)** → the workspace is on the split. Report success + the message name; it's now available to attach to campaign steps in the Enginy campaign editor (the public API doesn't attach AI Messages to campaigns — that linkage happens in the app; see https://docs.enginy.ai). Continue to Phase 6.\n- **403 \"AI variable split is not enabled for this workspace\"** → legacy workspace. Go to Phase 5.\n- **409** → name already exists; pick another name.\n\n**Static alternative:** if the user wants fixed, pre-written message copy (not AI-written per contact), use `create_a_message_template` instead (`channel`, ordered `messages[]`, `subject` for EMAIL/LINKEDIN_INMAIL) — same 403-fallback behavior applies; on legacy workspaces put static copy directly into campaign steps via **launch-campaign**. Templates may embed AI snippets and AI research via `{aiSnippet:<id-or-name>}` / `{aiResearch:<id-or-name>}` tokens.\n\n**Managing existing AI Messages (split workspaces):** the full CRUD surface exists — `list_ai_messages` (paginated), `get_an_ai_message`, `update_an_ai_message`, `delete_an_ai_message` (message templates have the same set). Read responses return the prompt both as rich text and as round-trippable plain token text, so the edit loop is: get → edit the plain-text prompt → update (all fields optional, at least one required; token rules re-validated). Deletes are soft and return 409 while the entity is still referenced (active campaign steps, other AI entities embedding it) — detach first. Enginy-default entities can't be edited or deleted (403).\n\n### Phase 5 — Legacy fallback (AI Variables system)\n\nTell the user plainly: \"Your workspace is on the legacy AI Variables system (the AI split hasn't been enabled for it yet), so I'll set this up as an AI variable — same result, different plumbing.\"\n\n1. Create a CONTACT AI variable via `create_an_ai_variable` (type `text`) whose prompt writes the message body: fold the channel norms, tone, and length directly into the prompt text (legacy variables have no `channel`/`toneId`/`outputLength` settings — the prompt carries all of it). Same placeholder rules (Phase 3), but **no `{aiResearch:<id>}` tokens** — reference other AI-variable values by their `{fieldName}` instead.\n2. Sample-test it exactly like a research field: `start_an_actions_run` with `FILL_LEAD_WITH_SMART_FIELDS` on 5–10 contacts (credits — quote via `get_credit_pricing` / `get_credit_balance` and confirm first), review, iterate via `update_an_ai_variable`.\n3. Use it in campaigns as a `{fieldName}` placeholder inside step content (**launch-campaign**) — on the legacy system the \"AI message\" is a variable dropped into the step copy.\n\n### Phase 6 — Wire into the campaign\n\nRoute to **launch-campaign** to build/update the sequence. Split workspaces: attach the AI Message to the step in the campaign editor. Legacy workspaces: the step content includes the variable placeholder. Either way, verify with a small test cohort before activating, and monitor results via **campaign-performance-analyzer**.\n\n---\n\n## Enginy's prompt-authoring standard (for message prompts)\n\nThe prompt that generates the message runs per contact across the whole list, so the same discipline that keeps research fields reliable at scale keeps message copy from breaking. Apply these when writing the `prompt` (Phase 4) — or the AI-part prompts in a bundle.\n\n**Recommended prompt structure.** Write the message prompt in this fixed order (skip sections a given message doesn't need, but keep the ordering): **CONTEXT** (who's writing, to whom, why) → **VARIABLES** (the only place `{fieldName}` / `{aiResearch:<id>}` tokens appear) → **GOAL** (the one outcome — book a call, get a reply, earn a connection) → **INSTRUCTIONS** (how to write it) → **RULES** (hard constraints incl. length norm and gap handling) → **SEARCH INSTRUCTIONS** (how to reason about the signal — internal, not output) → **DECISION HEURISTIC** (which angle/framing to lead with — internal, not output) → **OUTPUT FORMAT** (emit only the message text; for EMAIL, whether a subject line is included) → **EXAMPLE** (one worked message) → **LANGUAGE** (lock the output language).\n\n**Placeholders in VARIABLES only.** Declare each `{fieldName}` / `{aiResearch:<id>}` once under VARIABLES; everywhere else refer to it in plain words (\"the contact's role\", not `{jobTitle}` sprinkled through the instructions). Repeating raw tokens inline is what breaks formatting at scale — one oddly-rendered field can corrupt the whole message.\n\n**Missing-data discipline.** A message must never go out with a hole in it. Instruct the prompt: if a personalization input is missing or thin, fall back to a safe generic phrasing rather than emitting an empty slot, a literal token, or \"N/A\". Spell out the fallback wording in the RULES section (e.g. \"if no recent signal is found, open with a role-relevant observation instead\"). In any list, a fraction of contacts will be missing any given field.\n\n**Output reliability.** Across a list, some contacts will have thin or noisy data — make every message prompt robust to that:\n- **Format reliability.** Instruct the model that when personalization inputs are insufficient, it should use the safe generic fallback phrasing defined in RULES (a role-relevant, respectful opener) rather than emitting meta-commentary like \"I don't have enough information\". An off-format output is an unsendable message, so the fallback path must produce the same clean format as the happy path.\n- **Output-language lock.** End with an explicit language instruction (\"Write in English\") — otherwise the language drifts to match the contact's site/profile and the campaign ships mixed-language copy.\n- **No marketing-language signals.** Forbid treating generic self-promotion on a website (\"innovative\", \"cutting-edge\", \"AI-powered\") as a real personalization hook — it's on every site and produces hollow, obviously-templated lines. Personalize only on concrete, checkable facts.\n\n**Responsible-sending context.** These messages are sent through the user's own connected sender identities to prospects the user has sourced and qualified, within Enginy's per-identity sending limits, and Enginy's blocklist and global opt-out lists are always enforced at send time. The goal of per-contact AI writing is fewer, better messages — genuinely relevant one-to-one copy — not higher volume; steer users toward small, well-targeted cohorts and quality iteration (sample, review, refine) rather than broad blasts.\n\n---\n\n## Enginy MCP tools used\n\n- `create_an_ai_message` — create the AI Message (split workspaces; 403 = legacy signal)\n- `list_ai_messages` / `get_an_ai_message` / `update_an_ai_message` / `delete_an_ai_message` — manage existing AI Messages (split workspaces)\n- `list_ai_message_tones` — valid `toneId` values (workspace-owned + Enginy defaults)\n- `create_a_message_template` — static multi-message template alternative (same gating; has the same list/get/update/delete set)\n- `list_public_promptlibrary_ai_message_entries` — prompt patterns + `model`/`outputLength` baselines\n- `get_contact_field_metadata` — valid `{placeholder}` names\n- `list_ai_variables` / `get_an_ai_variable` — ids for `{aiResearch:...}` tokens\n- Legacy fallback: `create_an_ai_variable`, `update_an_ai_variable`, `start_an_actions_run` (`FILL_LEAD_WITH_SMART_FIELDS`), `get_actions_run_status`, `get_credit_pricing` / `get_credit_balance`\n\n---\n\n## Important Notes\n\n- **The 403 is the detection mechanism, not an error.** There is no client-readable flag for the AI split; attempting the create and branching on 403 is the documented pattern. Always relay to the user which system their workspace turned out to be on.\n- **Full CRUD on split workspaces:** list/get/update/delete exist for AI Messages and message templates; reads round-trip the prompt as plain token text for editing. Deletes are soft and 409 while referenced; Enginy-default entities are read-only (403 on edit/delete).\n- **`toneId` comes from `list_ai_message_tones`;** `model`/`outputLength` baselines from public prompt-library entries. Don't invent values.\n- **Reference policy:** AI Messages may embed `{aiResearch:...}` and `{aiMessage:...}` tokens (id or name); snippets are embedded in the app composer, not via message-prompt tokens. Split-workspace-only — on legacy, reference other variables by `{fieldName}`.\n- **Tokens are strictly validated (400 on unknown/ambiguous)** with `details.issues` + `details.validTokenSample`; escape literal braces as `{{`/`}}`. (The legacy `create_an_ai_variable` path is more permissive — still validate against field metadata there.)\n- **WhatsApp channel** requires WhatsApp to be enabled on the workspace; the create can 403 for that reason independently of the split flag — read the error message to tell the two apart.\n- **Legacy fallback runs cost credits** (`FILL_LEAD_WITH_SMART_FIELDS`) — always quote and confirm before running samples or lists.\n- **Rate limit:** 30 req/min on AI-variable-scope writes.\n\n---\n\n## Examples\n\n**Example 1 — AI first-touch email (split workspace)**\nUser: \"Have the AI write a personalized first email for each contact in my SaaS CTO list.\" → intent/channel: EMAIL first touch → `list_public_promptlibrary_ai_message_entries` → reuse toneId/model/outputLength from \"Value-First Intro (Email)\" → `get_contact_field_metadata` confirms `{firstName}`, `{companyName}`, `{jobTitle}` → prompt embeds `{aiResearch:812}` (a \"recent funding\" research field built earlier) → `create_an_ai_message` succeeds → user attaches it to the campaign's email step in the app → launch-campaign for the rest.\n\n**Example 2 — Same request, legacy workspace**\nSame setup → `create_an_ai_message` returns 403 \"AI variable split is not enabled\" → explain the legacy system → `create_an_ai_variable` (CONTACT, text, prompt carrying the email-writing instructions + tone/length norms inline) → sample `FILL_LEAD_WITH_SMART_FIELDS` on 8 contacts after credit confirmation → iterate → step content in launch-campaign uses `{firstEmailBody}`.\n\n**Example 3 — LinkedIn connection note (split workspace)**\nUser: \"AI-written connection requests, max 300 characters.\" → channel LINKEDIN_CONNECTION → baseline from a \"Connection Note\" library entry → prompt constrains to one signal + no pitch, 1–2 sentences ≤300 chars → `create_an_ai_message` with `outputLength` low → attach in app; sequence structure via launch-campaign (`linkedin_connection` step).\n\n**Example 4 — LinkedIn message bundle (Mode 2)**\nUser: \"A LinkedIn touch that's part manual, part AI — feels handwritten.\" → Mode 2 bundle → present Sequence Blueprint: Part 1 manual opener (\"Hi {firstName}, been following what you're building\"), Part 2 AI (1–2 sentences referencing a recent signal via `{aiResearch:<id>}`), Part 3 manual soft CTA → user confirms blueprint → propose 3 angles for Part 2, user picks the trigger-event angle → author only the Part 2 prompt (fixed skeleton, VARIABLES-only tokens, language lock, safe fallback) → `create_an_ai_message` for the AI part → wire the whole bundle into the `linkedin_message_bundle` step via launch-campaign.\n\n---\n\n## Troubleshooting\n\n| Problem | Fix |\n|---|---|\n| 403 \"AI variable split is not enabled for this workspace\" | Not an error — legacy workspace. Use the Phase 5 fallback (`create_an_ai_variable`) and inform the user |\n| 403 on WHATSAPP channel only | WhatsApp isn't enabled for the workspace — different gate than the split; pick another channel or enable WhatsApp (https://docs.enginy.ai) |\n| 409 Conflict | An entry with this name exists — rename |\n| Don't know a valid `toneId` | `list_ai_message_tones`; `model`/`outputLength` from a matching prompt-library entry |\n| Create/update 400 \"invalid prompt tokens\" | Unknown or ambiguous token — read `details.issues`, pick from `details.validTokenSample`, or use the id form `{aiResearch:<id>}` when a name is ambiguous |\n| `{aiResearch:...}` not accepted | Split workspaces only; must be a real entry (`list_ai_variables`); on legacy use `{fieldName}` |\n| Need to edit an AI Message after creation | `get_an_ai_message` (prompt returns as plain token text) → edit → `update_an_ai_message` |\n| Delete returns 409 | The message is still referenced (campaign step or another AI entity) — detach it first, then delete |\n| New CRUD/tones tools not in your tool list | Your MCP session predates the rollout — reconnect to refresh the tool catalog |\n| Legacy fallback output too long/wrong format | Legacy variables carry format rules in the prompt itself — tighten the prompt via `update_an_ai_variable`, re-sample |\n"
}

SHA-256: 4b1299f54a2d91c2effe10373618ee05f20543011ea8403c1149f7dd835b6418