The median AI skill ships 2 files. Mine ships 156.

Martin Shein · · 15 min read

I pulled apart 109 AI marketing skills before I built my own. The median one contains two files. Not two thousand lines, not two directories. Two files.

Mine contains 156. Here is exactly where they go, and why the difference is not vanity.

The thing most AI skills actually are

I want to be precise about the claim, because "most AI skills are bad" is the kind of line anyone can write and nobody can check.

So I counted. The corpus is 109 skills across six public marketing and go-to-market repositories: the kind of thing you install when you want an agent to do outreach, write posts, or run a campaign. For each one I counted every file in its subtree.

  • Median: 2 files

  • Mean: 3.6 files

  • Largest single skill in the corpus: 34 files

  • Skills shipping zero executable code: 91 of 109

That last number is the one that changed how I build. Eighty-three percent of these skills are prose. They tell the agent what good looks like and then trust it. There is no step anywhere in them that measures the output and refuses it.

Here is why that matters: an agent that has been told "keep subject lines short" will write a short subject line most of the time. Most of the time is not a standard. When it drifts, nothing catches it, and the first person to notice is the prospect who got the email.

Two more numbers from the same corpus, both about the direct competition. The third-party skill that shares my skill's name and purpose ships 13 files. The cold-email skill I benchmarked against ships 7. The email-sequence one ships 5. The lead-magnet one ships 3.

One correction, because the honest version is stronger

I nearly published a sharper line than the truth. The draft of this argument said that none of the third-party skills ship a grader. That is false, and I checked before it went out.

One repository in the corpus ships content-quality-gate.py and content-quality-scorer.py inside its content-ops skill. The first is described by its own author as a gate that nothing goes live without passing. The second scores five dimensions including voice similarity and AI-slop detection. That is conceptually the same idea as the graders I am about to walk you through. Somebody else got there independently.

The accurate version is narrower and, I think, more interesting. Of the 18 skills in the corpus that ship any executable code at all, every one of them comes from that same single repository. And within the outbound category specifically, nobody grades. The competing outbound skill ships five scripts and all five of them are doers, not checkers: an audit tool, a competitive monitor, a sender, a lead pipeline, and a signal detector. Code that acts. None that checks the work before it acts.

A rival converging on the same idea in an adjacent category does not weaken the argument. It is the best evidence I have that the idea is right.

The reframe: a skill is a system, not a prompt

Here is the thing: a single markdown file cannot do the job because the job has layers that contradict each other.

A skill has to be small enough to load into a context window at session start, and complete enough to answer a question it has never seen. It has to carry general principles and specific exemplars. It has to know how to do the work and how to check the work, and those are different kinds of knowledge that rot at different speeds.

You cannot solve that in one file. You solve it by splitting the file into layers where each layer has one job and its own refresh cadence. Which brings me to the tree.

A. The directory structure

This is the real thing, at version 3.0.0. Six directories, 156 files.

outbound-engine/
├── SKILL.md            the router — decision tree only, no procedures
├── schema.yaml         the typed method surface
├── instructions/  (2)  AGENTS.md · DEPLOYMENT.md
├── learnings/    (11)  one directory per hard-won rule, with its evidence
├── recipes/      (12)  one per job the skill is actually asked to do
├── references/   (13)  the procedures — loaded on demand, never all at once
├── registry/     (47)  installable exemplars: real frameworks, real templates
└── scripts/       (7)  the graders

Let me take those in the order they matter.

SKILL.md is a router, not a manual. It holds the decision tree and nothing else. When the agent gets a request it reads this file, decides which recipe applies, and loads only that. This is the single most important structural decision in the bundle. If the router inlines the procedures, every session pays the full token cost of every capability, used or not.

references/ holds the procedures. Thirteen of them, and their names tell you the shape of the problem: deliverability-2026.md, idap-personalization.md, multilingual-outreach.md, sequence-shapes.md, sender-integration.md, icp-saturation.md, writing-frameworks.md. These are loaded on demand. A campaign to a German list loads the multilingual reference; a domain warmup question loads the deliverability one. Neither loads the other.

registry/ is the layer nobody ships, and it is 47 of the 156 files. These are installable exemplars, and they are named things with real provenance rather than patterns I invented: AIDA. Dan Kennedy's PAS. Gary Halbert's coat-of-arms letter. Frank Kern's 4P. Dean Jackson's nine-word email. Andre Chaperon's storytelling sequence. Andy Maslen's PPP. The Belkins four-wave manufacturing sequence. The registry's own index breaks the 47 down as 14 frameworks, 19 templates, 9 blueprints, and 5 blocks.

Here is why that matters: an agent asked to write a cold email from a prompt writes the average of every cold email in its training data. An agent asked to write a cold email in the shape of Dean Jackson's nine-word email, with the exemplar in front of it, writes something with a spine. The skill picks the framework for the ICP and tells you why it picked that one. That reasoning step is only possible because there are 47 real options to pick between.

learnings/ is 11 entries, and each one is a rule that cost something. They are dated and named for what they assert: first-follow-up-is-the-biggest-lever, personalization-is-the-5x-multiplier, sequence-decay-after-5-emails, soft-cta-beats-hard-cta-first-touch, scale-mailboxes-not-per-mailbox-volume, anti-repetition-converts, framework-fit-by-icp. Three of them exist purely to defend a threshold used by a grader. So when a script fails your draft at a particular number, there is a directory on disk explaining where that number came from.

recipes/ is 12 files, one per job. Not per feature. cold-email-sequence.md, follow-up-no-reply-7d.md, reply-handler-meeting-booking.md, unsubscribe-detection.md, set-up-the-outbound-board.md, personalize-per-lead-from-idap.md. The test for whether something deserves a recipe is whether a person would ask for it in those words.

B. The verification scripts

Now for the part that actually separates this from a prompt.

First, an honest accounting, because two of my own sessions disagreed about this number and I had to settle it. The scripts/ directory contains eight files. Seven of them are graders. One is not.

The rule I used is empirical rather than cosmetic: a file counts as a grader if running it produces a grader surface. Run each of the eight with no arguments. Seven print a usage line or a verdict. The eighth, _verify-coordination-shared.mjs, prints nothing at all — it exports three functions and is imported by one of the other seven. It is a vendored library, not a check. So: seven graders.

They have zero third-party dependencies. Every import across all seven resolves to a Node builtin — node:fs, node:dns, node:path, node:url — or to a sibling in the same directory. There is no install step and no lockfile. For comparison, the competing outbound skill ships a requirements.txt.

Here is what they do, and I would rather show you than describe it.

verify-subject-line on a real subject from the test fixtures:

$ node verify-subject-line.mjs "{{first_name|there}}, a finance benchmark"
subject: "{{first_name|there}}, a finance benchmark"
  41 chars (≤50) · 4 words (≤7) · SWEET SPOT · token✓
  ✓ personalization token present (+30.5% response — Backlinko)
  → PASS

And the same script on a subject line of the kind an unsupervised agent produces at 2am:

$ node verify-subject-line.mjs "FREE!!! Act Now — Guaranteed 10x Your Revenue Today, Limited Time Offer"
  71 chars (≤50) · 12 words (≤7)
  ✗ length 71 chars > 50 (mobile truncation + dilution)
  ✗ word count 12 > 9 (Belkins: 9-10 words = lowest ~34-35% open)
  ✗ ALL-CAPS word(s): FREE!!! (reads as spam)
  ✗ repeated punctuation (!!! / ??? / …) — spam signal
  ✗ salesy/offer word(s): free, guarantee, guaranteed, act now, limited time
  ⚠ no personalization token (soft miss — not a fail)
  → FAIL

Look at what those failures carry. Not "this seems spammy." Five specific defects, each with a threshold. And the thresholds cite sources: Backlinko on personalization tokens, Belkins on word count, ColdMailOpenRate and Gong on salesy vocabulary. The grader is not expressing an opinion. It is applying research, and the learnings/ directory holds the receipts.

verify-sequence-variety is the one I am most attached to, because it catches a failure that is genuinely hard to see by eye. Give an agent a five-touch sequence to write and it will quietly repeat itself — email four re-uses email two's angle in different words. Here it is on a fixture I deliberately doctored to do exactly that:

$ node verify-sequence-variety.mjs --file sequence.json
sequence variety: 5 touches · 3 distinct pain points
  ✗ reused pain point across touches: "month-end close dragging past eight days"
  ✗ reused CTA across touches: "worth a look?"
  ✗ touches 2 & 4 have near-duplicate subjects (100% overlap)
  → FAIL

Same script on the undoctored original:

sequence variety: 5 touches · 4 distinct pain points
  ✓ every touch changes the angle · soft T1 · breakup ending · no dup subject/CTA/pain-point
  → PASS

verify-deliverability does live DNS. It is not a checklist that asks whether you remembered SPF; it looks:

$ node verify-deliverability.mjs di-atomic.com
deliverability: di-atomic.com
  SPF ✓ · DKIM ✓ · DMARC ✓ · safe cap 40/mailbox/day
  ⚠ DMARC p=none (monitor-only) — move toward quarantine/reject after monitoring
  ready=true → WARN

It probes seven common DKIM selectors and reads the actual DMARC policy, then tells me my own domain is on monitor-only. That warning is about my domain, in an article on my site. I left it in because a grader that only ever agrees with you is decoration.

verify-broadcast-issue is the least glamorous and the most useful. Its header comment says every check it makes was a real production bug, and that is true — each line is something that shipped wrong once. Here it is on a draft carrying two of them:

$ node verify-broadcast-issue.mjs --file issue.json
broadcast issue: broadcast.product · subject 23 chars · body 115 words
  ✗ body's first line repeats the subject — the subject already renders as
    the H1, so it would appear twice. The body is the MIDDLE section only.
  ✗ body contains {{merge_tags}} — that's smartlead syntax and renders
    literally here. Broadcast personalization is {firstname}, injected by
    the template.
  → FAIL

Fix the two, rerun, and it passes. That second failure is the one worth dwelling on, because it is where the graders meet the send layer.

Three more round it out. verify-cold-email-length enforces a 50-to-125-word band, and says in its own output that passing length is necessary rather than sufficient. lint-spam-triggers scores structural signals and vocabulary. It also prints a useful reminder: warmup and authentication move inbox placement 30 to 50 points, while word choice moves it 0 to 5. And verify-coordination checks the skill's own wiring to the platform services underneath it.

C. The send layer

The skill drafts. It does not send. That separation is deliberate and it is where smartlead and SpiderMail come in.

The subtle part is the one I got wrong for a while. Smartlead is two different products depending on which mode you are in.

In cold-volume mode, smartlead is the sender. It owns the campaign, the sequence steps, the spintax, and the pacing across a warmed mailbox pool. The skill emits merge tags in smartlead's exact syntax, always with fallbacks, so a missing field renders "Hi there" rather than "Hi ,". It then maps its own step-subject-body-delay shape onto smartlead's campaign step model and hands it over.

In cold-agent mode, smartlead is warmup infrastructure only. It keeps the mailboxes reputable and SpiderMail does the actual sending, one bespoke agent-written message per lead. Getting this wrong has a specific, ugly failure: a draft targeted at smartlead when the mode is cold-agent carries {{merge_tags}} and spintax into a per-message send that never interpolates them. The prospect receives literal curly braces.

That is the same class of bug verify-broadcast-issue catches, which is why it is a check and not a warning in a document.

SpiderMail handles the per-message path and, more importantly, the reply path. It takes markdown and converts to HTML, so the drafter emits markdown and never raw HTML. Replies thread on a numeric message id read back from the inbox, not on the RFC Message-ID string. Its outbound scanner hard-blocks any send containing a key or a bearer token, which means a credential that leaks into a draft fails the job rather than reaching a stranger. And the skill reports "drafted and handed to SpiderMail," never "sent," until the sender confirms — because sendEmail returns a job id and queued is not delivered.

One thing I want to state plainly rather than let you infer it. SpiderMail is wired into this skill through the reference layer — nine files in the bundle describe the contract, and references/sender-integration.md is the real specification. But it is not in the package's declared peer_dependencies. The nine declared peers are listed below and SpiderMail is not among them. I found that gap while writing this article and it is logged as a defect against the skill rather than papered over here.

D. Composition — the part nobody copies in a weekend

If you rebuilt the tree above over a weekend, you would have a good skill. You would not have this one, because the layer that matters most is not files at all.

The package declares nine peer dependencies with version floors. Four are the platform services that give the skill state:

  • @opvs-ai/agentboard — the board. One card per lead, provisioned by the skill rather than assumed to exist.

  • @opvs-ai/agentdocs — where the drafted sequence and the strategy rationale get persisted as a versioned page.

  • @opvs-ai/agentmemory — recalled at the start of strategy planning, so the framework that worked for this ICP last time is an input rather than a rediscovery.

  • @opvs-ai/opvs-protocol — hands the running campaign off to a standing sender agent.

Five are sibling skills that each own one specialism:

  • @di-atomic/icp-discovery — who to email.

  • @di-atomic/voice-builder — the register to write it in.

  • @di-atomic/hook-generator — the subject and the opener.

  • @di-atomic/copy-engine — the bodies.

  • @di-atomic/smartlead — the send.

Here is why that is the hard part. A thin skill hands you a prompt and wishes you luck. This one chains specialists across the whole run. Who to email. What voice to write in. How to open. Which framework the body is built on. Who sends it. And where the state lives when the reply lands three days later. Every one of those peers is a shipped, versioned package with its own registry and its own graders. The composition is not a diagram in a README. It is a version floor in a manifest that fails to install if the peer is missing.

You can copy a directory tree. You cannot copy nine dependencies that do not exist yet.

What this means if you are building one

Bottom line: the gap between a two-file skill and this one is not effort, it is architecture. Four decisions did most of the work.

  1. Make the entry file a router. If it contains procedures, every session pays for every capability.

  2. Ship exemplars, not adjectives. "Write a compelling subject line" is worth less than one Dean Jackson nine-word email sitting on disk with its provenance attached.

  3. Write the graders before you need them. Every check in my scripts/ directory exists because something shipped wrong once. The cheapest time to add one is the day it breaks.

  4. Decide what your skill will refuse to do. Mine drafts and refuses to send. That boundary is what let the send layer become someone else's problem, properly specified.

And one anti-lesson, from this article's own research: I almost shipped a claim that was sharper than the evidence. Checking it cost twenty minutes and cost me the better line. The narrower true version is the one above. If you build in public, budget for that trade — it happens more often than you would like.

Get my outbound skill

I am giving it away. It runs in any agent you already use, on the desktop or in the terminal, and the graders have no dependencies to install.

The public download is going up shortly and I will link it from this page the moment it is live. If you want it before then, message me and I will send it over.

And if you would rather talk about the outbound problem underneath it than the tooling on top of it, that is the conversation I actually enjoy. I am Martin Shein, and I build these at Di-Atomic.