microsoft/competitive-battlecard-builder
>- Build an interactive sales battlecard comparing your product or company where you're genuinely vulnerable, and objection-handling scripts. Use this skill whenever someone asks for a battlecard, competitive positioning, "how do we beat [competitor]", objection handling against a named competitor, or a sales enablement comparison asset. Also use when someone wants to add a competitor to an existing battlecard, update it with new intel, or rescope/filter an existing one down to specific competitors or categories.
npx skills add https://github.com/microsoft/cat-agent-skills --skill competitive-battlecard-builder
The interactive app output and the PowerPoint export both require Code Interpreter available to the agent. Without it, present the battlecard as formatted chat text instead.
When writing Python for Code Interpreter, avoid building scripts through shell heredocs or string concatenation that has to escape the battlecard data inline, especially once brand colors, hex codes, or parenthesized function calls like RGBColor(...) are involved, unbalanced quotes or parens in the data will break the heredoc before your code ever runs. Write the JSON data to its own file (json.dump) and have the script json.load it, or write the whole script as a single Python file directly rather than assembling it through shell quoting.
objections array), since that's inherently a sales concept, what a prospect pushes back with and how to respond.Optionally ask for category tags (e.g. Pricing, Security, Integration, Support, Ecosystem) if they want the comparison filterable by category as well as by item. If they don't have a preference, pick 3-5 sensible categories based on the content itself.
.pptx template file, or at minimum the core details: primary/secondary colors (hex if they have them), font names, and a logo if relevant. If no, say plainly that you'll use a clean, generic default, and move on, this isn't worth belaboring if they don't have one.A battlecard a sales rep can't trust is worse than no battlecard, it gets someone caught flat-footed in front of a customer. Every single claim in this skill's output carries a source ("user" or "web") and a verified flag:
source: "user", verified: true.source: "web", verified: false until the person explicitly confirms it.Never invent a competitor claim. If you don't have it from the person or a real search result, leave it out rather than filling the gap with something plausible-sounding.
Before generating the final deliverable, if there are any web-sourced claims, show the person exactly what you found, don't summarize or paraphrase it away. For each one, list which competitor it's about, which section it would go in (pitch, win, vulnerability, or objection), and the claim text itself, close to verbatim from the source. End with something low-friction like "Let me know if anything's off and I'll fix it before it goes into the [html app / deck]." A person should be able to skim the list and immediately spot anything wrong, not have to reconstruct what you actually found from a vague summary.
Only after that review does a claim get treated as trustworthy in the shipped battlecard. Claims that remain unconfirmed (the person didn't respond to that specific one, or explicitly said "not sure") should either be dropped or clearly marked unverified in the output, never silently presented as settled fact. If they flag something as wrong, correct or remove it and don't carry it into Step 3 as-is.
The two modes need genuinely different data shapes, not just different labels on the same one. Battlecard mode is inherently pairwise, "us" versus each competitor, one at a time. Neutral mode is not: there's no anchor item, every item being compared is a peer, so the data model is a real matrix, one shared set of dimensions with a value for every item, not a series of us-vs-X comparisons.
mode is "battlecard"{
"mode": "battlecard",
"us": { "name": "string", "tagline": "string" },
"categories": ["Pricing", "Security", "..."],
"competitors": [
{
"id": "slug",
"name": "Display name",
"theirPitch": { "text": "string", "source": "user|web", "verified": true },
"comparison": [
{
"category": "one of categories[]",
"dimension": "Short label for what's being compared, e.g. 'Recording capture method'",
"us": { "text": "Our position on this dimension", "source": "user|web", "verified": true },
"them": { "text": "Their position on this dimension", "source": "user|web", "verified": true },
"edge": "us|them|even"
}
],
"objections": [
{ "objection": "What the prospect says", "response": "How to handle it", "source": "user|web", "verified": true }
]
}
]
}
mode is "neutral"{
"mode": "neutral",
"items": [
{ "id": "slug", "name": "Display name", "positioning": { "text": "string", "source": "user|web", "verified": true } }
],
"categories": ["Pricing", "Security", "..."],
"matrix": [
{
"category": "one of categories[]",
"dimension": "Short label for what's being compared",
"values": {
"item-id-1": { "text": "That item's position on this dimension", "source": "user|web", "verified": true },
"item-id-2": { "text": "That item's position on this dimension", "source": "user|web", "verified": true }
},
"best": "item-id-of-whichever-item-leads-this-dimension, or null if genuinely even"
}
]
}
items needs at least two entries, values on every matrix row should have an entry for every item in items (missing ones render as blank/unverified rather than breaking anything, but that means incomplete data, go back and fill the gap). Never use the word "competitor" anywhere in neutral-mode language, in what you say to the person or in the data itself, there isn't one, that's the whole point of this mode.
mode from what they told you in Step 1. This isn't cosmetic, it changes both the data shape above and how the app renders and labels everything.edge in battlecard mode, best in neutral mode). Not every dimension has to favor the same side, and "even" / null is a legitimate, credible answer, not a cop-out. A card where every single row favors the same item reads as marketing, not something anyone can trust.category must match one of the top-level categories[] values exactly, the filtering keys off this.comparison/matrix rows grouped by category, all rows for one category together before moving to the next. The app inserts a category header whenever the category changes from the row before it, not once per unique category, so an interleaved order (Pricing, Security, Pricing) produces a duplicate "Pricing" header instead of one clean group.text/response field a plain sentence a rep could say out loud, not a bullet fragment.id values must be unique, stable slugs (used for filtering and DOM references, not shown to the user).Using Code Interpreter:
assets/battlecard-app-template.html.<style> block (--blue, --green, --red, --amber, and their -light companions) to match. In battlecard mode, --green/--red drive the win/loss highlighting, keep those two visually distinct from each other regardless of the brand palette, the highlighting only works if someone can tell the colors apart at a glance. In neutral mode the matrix only ever uses --blue for its single neutral highlight, there's no red/green to worry about there. If they gave you a logo, you can add it next to the title, but don't restructure the layout to accommodate it. If they didn't give you branding, leave the defaults as-is, don't invent a color scheme.</ with <\/ before inserting it. This prevents an item name or claim containing the literal text </script from prematurely closing the embedded <script> tag. import json
json_str = json.dumps(battlecard_data).replace("</", "<\\/")
{{BATTLECARD_DATA_JSON}} in the template with the escaped string..html, named after what's being compared (e.g. battlecard-vs-competitor-name.html for battlecard mode, comparison-item-a-vs-item-b.html for neutral mode).Do this through Code Interpreter so it comes back as an actual file with a download link, printing raw HTML into the chat as text won't render it, the platform will just show the literal markup. If Code Interpreter isn't available, present the same content as formatted chat text instead: battlecard mode grouped by competitor with us/them pairs per dimension plus objection handling; neutral mode as a single table with one row per dimension and one column per item.
If the person later wants a PowerPoint version too, that's a separate follow-up ask, go to Step 4b at that point using the same underlying data.
If the person gave you a .pptx branding template, open it as the base presentation (Presentation("their_template.pptx") in python-pptx) and add your slides using their existing slide masters and layouts, so the deck inherits their fonts, colors, and placeholder positions rather than starting from a blank default. Only fall back to building a plain default presentation if they didn't provide one.
If they only gave you colors/fonts (no template file), apply those to a default layout: use their font for headings and body text, and use their primary/secondary colors for the title slide and the "who has the edge" table highlighting, keeping the win/loss colors visually distinct from each other regardless of the palette.
A python-pptx gotcha worth knowing before you hit it: RGBColor is a tuple subclass, it has no .red/.green/.blue attributes, indexing into it (color[0], color[1], color[2]) works but attribute access throws. If you need a lighter tint of a brand color for cell shading (the same idea as the HTML app's -light color variants), do the math on plain integers before constructing the RGBColor, not after:
def lighten(hex_color, amount=0.85):
hex_color = hex_color.lstrip('#')
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
r = int(r + (255 - r) * amount)
g = int(g + (255 - g) * amount)
b = int(b + (255 - b) * amount)
return RGBColor(r, g, b)
If they gave you neither, use a clean, professional default, don't invent a brand identity, just keep it legible and uncluttered.
Don't ship a bare python-pptx table dump. Left unstyled, python-pptx tables render with PowerPoint's default banded-blue table style, small unstyled text, and no visual hierarchy, that reads as an obvious AI-generated afterthought, not something a rep would actually present from. Override the defaults explicitly:
python-pptx's tiny default size.If your sandbox can render the deck to an image before finalizing (for example, converting to PDF and rasterizing a page), do a quick visual pass for text overflow, overlap, or low-contrast text against its background before calling it done. If that's not available in your environment, apply the sizing and wrapping guidance above defensively, since you won't get a check on the actual output.
Structure it depends on which mode you're in, since the data shapes are genuinely different (see Step 3):
In "battlecard" mode: a title slide (your name vs the competitor(s)), then for each competitor, a comparison table with three columns (Dimension / Us / Them), one row per comparison entry, the edge winner's cell given a background fill, green tint for "us", red tint for "them", matching the HTML app's convention. Follow each competitor's table with a short objection-handling section (still text, that one doesn't fit a table format).
In "neutral" mode: a title slide, then one single matrix table, not a table per item. Columns are Dimension plus one column per entry in items[], in the same order every time. Rows come from matrix[], one row per dimension, and the best item's cell (if any) gets a single neutral accent fill, never red/green, since that would imply a bias that isn't there. If best is null for a row, no cell gets highlighted. There's no objection-handling section in this mode, there's no objections data to draw from.
In either mode, group rows by category if there are more than 5-6 comparison points, so it doesn't read as a wall of rows. If the matrix or a competitor's table has enough entries that one table would be unreadably dense (this gets tighter fast in neutral mode as items[] grows, a 5-column matrix eats width quickly), split by category across multiple slides rather than shrinking text to fit or cramming columns narrower than they can hold their content.
Carry the verification status into the deck. Any cell built from a claim with verified: false needs a visible marker in the cell text itself (append "(unverified)"), whether that's a us/them cell in battlecard mode or an item's cell in the neutral matrix. Don't drop the distinction just because it's no longer in an app with a badge, someone presenting from a printed or shared deck needs the same warning the HTML app gives them.
If the person later asks for a rescoped version ("just give me a deck for these two" or "only the pricing category"), regenerate a fresh .pptx from the same underlying data filtered to their request rather than trying to edit the existing file.
If the person later wants the HTML version too, that's a separate follow-up ask, go to Step 4a at that point using the same underlying data.
Take microsoft/competitive-battlecard-builder from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.