Contengi docs
Playbooks — the prose format
A Contengi playbook is a single markdown document. You write what should happen in plain prose, reference the available tools by name, and end with a strict JSON output schema. Claude reads the whole document as a system prompt and executes it agentically against the brand's data.
1. Shape of a playbook
- Plain markdown. Use
##for numbered steps. - Reference tools by name in the prose (Claude reads the name and calls the tool).
- Reference workflow variables via mustache:
{{primary_keyword}}. - Final step tells Claude to return ONE JSON object matching the BlogResult schema.
- You do NOT enforce brand voice, em dashes, or banned phrases. A post-pass handles all of that.
2. Variables you can reference
{{primary_keyword}}— the target keyword (string){{content_type}}— 'editorial' or 'listicle'{{user_context}}— optional user-provided angle / notes (may be empty){{brand_name}}— the brand the blog is being written for
The brand's full KB (sections, rules, examples) plus sitemap and approved external domains are also injected into Claude's system context automatically — no need to reference them as variables.
3. Available tools
Mention any of these by name in your playbook prose. Claude will call them when your step says to. All tools are read-only.
brave_searchSearch the web via Brave Search. Returns the top results with title, URL, and description. Use this for general web research, fact-checking, or finding additional sources.
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query."
},
"count": {
"type": "number",
"description": "Number of results to return (default 5, max 10)."
}
},
"required": [
"query"
]
}serp_researchrequires serpapi keyFetch Google SERP data via SERPApi: organic results, AI Overview (Google's AI-generated answer box), People Also Ask, and related searches. Use this when you need to understand what's currently ranking for a keyword AND what AI Overview is showing.
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query."
},
"location": {
"type": "string",
"description": "Optional location (e.g. \"United States\")."
},
"num_results": {
"type": "number",
"description": "Number of organic results (default 10, max 20)."
},
"include": {
"type": "array",
"description": "Which result types to include. Default: all.",
"items": {
"type": "string",
"enum": [
"organic",
"ai_overview",
"paa",
"related"
]
}
}
},
"required": [
"query"
]
}Per-step cap: 3 calls
fetch_urlFetch the text content of a public web page. Returns up to 3000 characters of plain text plus a list of outbound links. Used for reading sources, competitor pages, or any specific URL.
{
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Absolute http(s) URL to fetch."
}
},
"required": [
"url"
]
}check_url_404Check whether a URL is live (returns < 400 status code). Useful for validating links before embedding them.
{
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Absolute http(s) URL to check."
}
},
"required": [
"url"
]
}kb_lookupQuery the brand's knowledge base. Returns sections (brand_strategy, pov, audience, tone_of_voice, writing_guide, etc.), never/always rules, or content examples filtered by type.
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"sections",
"rules",
"examples"
],
"description": "What to look up: sections, rules, or content examples."
},
"section_type": {
"type": "string",
"description": "When kind='sections', filter by section_type. Omit to return all sections."
},
"agent_type": {
"type": "string",
"description": "When kind='examples', filter by agent_type (e.g. 'seo_blog')."
},
"limit": {
"type": "number",
"description": "Max rows to return (default 10)."
}
},
"required": [
"kind"
]
}transcript_lookupSearch the brand's transcripts (podcasts, webinars, calls, voice notes). Returns matching transcripts with title, source type, status, AI analysis (summary/key_quotes/content_angles), and a content preview. Use this to mine quotable insights, customer language, or expert commentary that can give a blog real specificity.
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Optional case-insensitive substring to match against transcript title or content. Omit to list the most recent."
},
"limit": {
"type": "number",
"description": "Max rows to return (default 5, max 20)."
},
"include_full_content": {
"type": "boolean",
"description": "If true, returns the full transcript content (can be long). Default false — returns a 600-char preview only."
}
}
}notes_lookupSearch the brand's notes & memos. Returns title, folder, content (or preview), source type, and any AI analysis. Use this to pull internal context, customer interviews, strategy memos, or anything the team has written down for this brand. By default skips personal (is_personal=true) notes.
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Optional case-insensitive substring to match against note title or content."
},
"folder_id": {
"type": "string",
"description": "Optional folder UUID to scope the search. Use list_folders first to discover folder IDs."
},
"limit": {
"type": "number",
"description": "Max rows to return (default 5, max 20)."
},
"include_full_content": {
"type": "boolean",
"description": "If true, returns the full note content. Default false — returns a 600-char preview only."
},
"include_personal": {
"type": "boolean",
"description": "If true, includes personal (is_personal=true) notes. Default false."
}
}
}list_foldersList the brand's notes folder hierarchy. Returns folder id, name, and parent_id. Use this to find a folder_id before calling notes_lookup with a folder filter.
{
"type": "object",
"properties": {}
}kb_files_lookupSearch the brand's uploaded knowledge-base files (PDFs, docs, etc. converted to markdown). Returns filename and a content preview (or full content if requested). Use this when the brand has reference material uploaded as files rather than KB sections.
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Optional substring to match against filename or content."
},
"limit": {
"type": "number",
"description": "Max rows to return (default 5, max 20)."
},
"include_full_content": {
"type": "boolean",
"description": "If true, returns the full markdown content. Default false — returns 800-char preview."
}
}
}fetch_sitemapFetch a sitemap.xml URL and return the list of <loc> URLs it contains. Useful for discovering internal-link candidates. If the brand has a sitemap section in their KB, that URL is provided in the system context already.
{
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Absolute http(s) URL to the sitemap.xml file."
},
"limit": {
"type": "number",
"description": "Max URLs to return (default 200, max 1000)."
}
},
"required": [
"url"
]
}gsc_insightsFetch Google Search Console insights for the brand for a given keyword: top queries, click-through rates, average position. Returns null if GSC is not configured for this brand.
{
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "The focus keyword to look up."
}
},
"required": [
"keyword"
]
}ga4_insightsFetch Google Analytics 4 insights for the brand: top landing pages, traffic patterns. Returns null if GA4 is not configured for this brand.
{
"type": "object",
"properties": {}
}4. The final output contract
Your playbook's last step must tell Claude to return ONE JSON object exactly matching this shape (no markdown fences, no preamble):
{
"title": "...",
"seo_title": "60 chars max, keyword-front-loaded",
"meta_description": "155 chars max, includes keyword, has a hook",
"excerpt": "1-2 sentence summary for blog listing pages",
"hero_intro": "2-3 punchy sentences before the main body",
"body_html": "HTML body with embedded <a> links + FAQ section",
"faq": [],
"external_links": [{"url": "...", "anchor": "...", "context": "..."}],
"internal_links": [{"url": "...", "anchor": "...", "context": "..."}],
"estimated_read_time": 4,
"target_keyword": "{{primary_keyword}}",
"secondary_keywords": []
}After Claude returns this JSON, Contengi automatically runs runTovRefinement + runCleanPipeline on body_html to enforce brand voice + anti-slop + hard rules. You don't need to handle any of that in the playbook.
5. Editing in Contengi
- Open SEO blog writer → Playbook settings (top right).
- You'll see the locked Contengi default - SEO blog (read-only safe fallback) and the editable Default + AI-snippet analysis example.
- Duplicate or create new, edit in the markdown editor, save. Click Set as preferred to auto-select your playbook on the queue form.
- Variables, tool names, and the final JSON contract are exactly as documented above.
6. Write playbooks in Claude Code (optional)
If you want to draft a playbook in Claude Code or any LLM chat, paste this system prompt first, then describe what you want the playbook to do.
You're helping me write a Contengi playbook — a single prose markdown
document that tells Claude how to produce an SEO blog end-to-end.
Format:
- Plain markdown, numbered sections (## 1. Step name).
- Reference tools by name (serp_research, kb_lookup, transcript_lookup,
notes_lookup, fetch_url, fetch_sitemap, gsc_insights, ga4_insights,
check_url_404, list_folders, kb_files_lookup, brave_search).
- Reference variables via {{primary_keyword}}, {{content_type}},
{{user_context}}, {{brand_name}}.
- The final step instructs Claude to return ONE JSON object matching the
Contengi BlogResult schema. Do NOT use markdown fences around that JSON
in your instruction.
Constraints:
- Tools are read-only.
- No em dashes, no emojis (Contengi enforces these via the final pass).
- Keep instructions specific. Vague playbooks produce vague blogs.
- Anti-slop + TOV refinement run automatically after the final JSON output.
You do NOT need to enforce voice or banned words in the playbook.
The playbook should do: <describe here>.
Output a single markdown playbook I can paste into Contengi's Playbook
settings editor.