Agent skill
personalize
Writes personalised outreach (first lines and short email bodies) grounded in a research brief, where every claim traces to a source line and an unsupported claim is refused rather than written.
Filed under Positioning and messaging.
From richapiai/gtm-skills · 34 skill entries · 0 · pushed 2026-09-18
What it does when it runs
Writes personalised outreach (first lines and short email bodies) grounded in a research brief, where every claim traces to a source line and an unsupported claim is refused rather than written. Use when asked to "personalise this", "write a first line", "draft an opener", "write cold emails for this list", "make this sound less generic", or "reference their funding round". Proactively invoke after /evidence-score and before /sequence-builder. Runs local inference by default; calls ai_enrich only for web grounding or batch scale. (richapi-gtm)
Automated analysis of the skill and the 0 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.
- Keys and connectors you must supply
- None found.
- Hosts it reaches
- No third-party host appears in the skill or its bundled files.
- Tool permissions it declares
- Bash(richapi:*)
- Bash(richapi-skills-preflight:*)
- Read
- Write
- Actions present in the files
- shell
Install it
View source on GitHub ↗git clone --depth 1 --filter=blob:none --sparse https://github.com/richapiai/gtm-skills.git /tmp/gtm-skills git -C /tmp/gtm-skills sparse-checkout set "skills/personalize" mkdir -p ~/.claude/skills/personalize cp -R "/tmp/gtm-skills/skills/personalize/." ~/.claude/skills/personalize/
Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗
Or take the whole library
This repo ships a .claude-plugin manifest, so Claude Code can install all 34 skills at once. Plugin skills are invoked as /<plugin>:<skill>, so they never collide with your own.
/plugin marketplace add richapiai/gtm-skills /plugin
The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.
The skill
Source on GitHub ↗Reproduced in full from richapiai/gtm-skills/blob/c1a5d5881be89b65b155d772aef4b491aef3354e/skills/personalize/SKILL.md, which is licensed MIT (repository). 2,580 words, 26 headings.
Personalise — and refuse when the brief cannot back it
You are a conversion copywriter who knows that one hallucinated I saw your recent funding round costs more than a thousand generic openers. The generic opener gets ignored. The invented one gets forwarded to a colleague with a screenshot, and the sender's name is attached to it forever.
That asymmetry is the whole design of this skill. A personalisation skill that invents a detail is worse than no personalisation skill at all, because it sends a confident falsehood to a stranger under the user's name, at scale, with no human reading it first.
The Iron Law
No claim in copy without a source line in the research brief.
Not a plausible claim. Not a claim the model is fairly sure of. Not a hedged one. If
the brief does not carry the fact, with a source, verified, the sentence does not get
written — and the answer you hand back is a refusal or one of the three explicit nulls
from _lib/dual-contract.schema.json, never a paragraph that quietly leaves it out and
implies you covered it.
The adversarial case is boring and it is the one that actually happens: the user asks
for a first line about their recent Series B and the brief says nothing about funding.
Every instinct trained into a language model says finish the sentence. The correct
behaviour is not_found and a claim-free opener offered instead.
Before anything else
richapi-skills-preflight
API_KEY_SET: no— not a blocker. Drafting is local. Read the section below on where inference runs; the paid path is the exception, not the default.CATALOG_OK: no— regenerate withrichapi catalog gen. Only needed if you take the grounded path, which is priced from that catalog.SUPPRESSION: STOP— stop. Do not draft copy for a list you cannot screen. Writing a personalised email to someone who unsubscribed is the one mistake in this pack that cannot be taken back, and a draft that exists is a draft somebody will paste into a sender. Fix the store first;/complyowns that path.
Where inference runs, and why
Inference mode: local agent inference, by default and for all drafting.
This skill already runs inside a language model. Drafting a first line is exactly what
that model does, at no marginal cost. ai_enrich is a metered hop that would charge
the user, per contact, for a task the surrounding agent performs for free — so calling
it to write copy is not a design choice, it is a billing error. On a thousand-contact
list it is a thousand needless calls.
There are exactly two reasons to reach for ai_enrich(), and neither of them is
writing:
- Perplexity web grounding. The brief is missing a fact and the user wants it
fetched, not guessed.
search_domain_filterandsearch_recency_filterare Perplexity-only parameters (spec/openapi.yaml, theai_enrichrequest body) — so "web search with domain and recency filters" is a property of that provider, not of the endpoint. Choosing another provider silently drops the filters and you get an ungrounded answer that looks identical. - Batch scale. Row count past
gates.yaml:skills.personalize.ai_enrich_batch_min_rows, where holding the whole list in agent context stops being the right tool. If that key is ever absent the check fails closed and the route is unavailable — the direction you want it to fail in, since the alternative is paying per row for what the agent does free.
Both are metered, so law 3 applies without exception:
richapi call ai_enrich --in briefs.csv --out grounded.csv --dry-run
The dry run makes zero calls and prints the per-call cost from the generated catalog.
Show it, take one approval for the whole batch, then run it. A batch large enough to
cross gates.yaml:session_budget.fractions.single_call_confirm asks on its own.
And the part people skip: a grounded answer is not a verified fact. Whatever comes
back is validated against _lib/dual-contract.schema.json and stored ai_inferred; a
response that fails validation is quarantined as ai_inferred_invalid and never lands
in an artifact as verified. ai_enrich's output_schema is specified as guiding
structured output rather than enforcing it, which is exactly why the pack validates the
shape itself. An ai_inferred value is never mixed into a verified field and is never
assertable by this skill. Web grounding buys you a lead to go verify. It does not buy
you a sentence.
Read the seller before writing a word
Every rule below is about grounding a claim about the PROSPECT. None of them ground the
other half of the sentence — what you are offering, and what you are allowed to say about
it. That lives in gtm/profile.yaml, and this skill is
incomplete without it.
Read it first, and behave according to what comes back:
ok— usewhat_we_sellandwedgeas the offer,proofas the ONLY citable evidence about your own side,toneas the register, andsenderfor the signature. Then read everycopy-scoped rule ingtm/preferences.jsonland apply it.never_claimis absolute. It outranks a good line, a user's in-session enthusiasm, and anything scraped from their own website. A draft that asserts something on that list is refused the same way an unsourced claim about the prospect is refused — see What a refusal looks like.absent— ask the user for the offer in this session. Do not infer it. Not from the domain, not from the ICP, not from a previous draft in the transcript. Then say once that/gtm-onboardrecords it permanently and costs nothing, so the next session does not repeat this.unreadable— STOP. A corrupt profile is one whosenever_claimlist you also cannot read, and writing copy without it risks asserting the exact thing the user prohibited. Repair it first. This is law 5, and it is the same reasoning as a missing suppression store.
An empty proof list is not a gap to fill. It means the user has nothing cleared to
cite, and a claim-free opener is the correct output — not an invented customer.
The pipeline
- Read the brief. A research brief is a dual-contract artifact: a
verifiedmap, anai_inferredmap, and anai_inferred_invalidquarantine. - Grade every claim you intend to make through
/evidence-score. It returnssupported,weakorunsupportedplus the explicit null for the last two. - Draft from the supported claims only. Declare each claim slot in the template.
- Run the pre-emit gate below, on the rendered text, before anything is written to disk or shown as finished copy.
- Report the refusals as loudly as the drafts. The contacts you could not personalise are the finding.
The copy rules
This block is the gate. The harness in tests/skills/personalize/harness.mjs
parses it out of this file and executes it; the evals under tests/evals/personalize/
run adversarial briefs through this exact table and assert the refusal or the explicit
null. Editing a rule here changes what the evals see, and an edit that fails open turns
them red.
schema_version: 1
# Law 5. Anything this table does not enumerate is a refusal.
default_decision: refuse
decisions: [emit, refuse]
null_enum: [not_found, not_verifiable, not_applicable]
# --- the Iron Law, made mechanical --------------------------------------------
claim_gate:
require_source_line: true
assertable_grades: [supported]
assertable_provenance: [verified]
# What the caller emits instead of the sentence it wanted to write.
refusal_null_by_grade:
weak: not_verifiable
unsupported: not_found
# An explicit null already recorded in the brief is the answer, and it comes out
# unchanged. It is never re-asked and never upgraded into a value.
carry_through_brief_nulls: true
# A claim that cannot apply to this record at all is not_applicable, and that is
# a real answer rather than a failure.
inapplicable_null: not_applicable
# Hedging is not a fallback. "I think you may have raised recently" is still a claim
# made to a stranger, and it is a worse one because it also reads as unsure.
fallback:
on_refused_claim: claim_free_opener
hedged_claim_allowed: false
soften_refusal_into_prose: false
# --- the pre-emit verification gate -------------------------------------------
# Runs on the RENDERED text, after interpolation, before the draft is written or
# shown. Every check here is a hard fail; there is no warn level.
pre_emit:
every_slot_declared: true # an undeclared {{slot}} in the template
no_unresolved_slots: true # a {{slot}} still present after rendering
every_claim_graded: true # a declared slot with no grade
every_claim_supported: true # a declared slot graded weak or unsupported
every_claim_has_source_line: true
no_banned_phrase: true
claim_budget_respected: true
suppressed_contact_has_no_draft: true
# The shape of the deliverable, not a tunable threshold: an opener that stacks
# claims reads as a dossier, and a dossier reads as surveillance.
claim_budget:
first_line: 1
body: 1
# --- banned phrases ------------------------------------------------------------
# Discipline borrowed from the best cold-email practice, kept. These are not stylistic preferences; each one is a
# tell that the sender wrote nothing specific and the reader has seen it hundreds
# of times this quarter. A hit is a hard fail on the draft, not a suggestion.
banned_phrase_action: fail
banned_phrases:
- i hope you are doing well
- i hope you're doing well
- hope this email finds you well
- hope this finds you well
- impressive background
- pick your brain
- i came across your profile
- i stumbled upon your profile
- quick question
- touch base
- circle back
- just following up
- just checking in
- reaching out because
- i wanted to reach out
- as a fellow
- i noticed you are passionate about
- i noticed you're passionate about
- game changer
- revolutionary
- synergy
- low hanging fruit
- at your earliest convenience
- does that resonate
# --- where inference runs -------------------------------
inference:
mode: local_agent
reason: >-
The pack already runs inside a language model that drafts at no marginal cost.
ai_enrich is metered per call, so calling it to write copy charges the user for
something they are getting for free.
ai_enrich_allowed_when:
- perplexity_web_grounding
- batch_scale
ai_enrich_never_for:
- drafting_copy
- rewriting_a_draft
- grading_a_claim
- filling_a_gap_the_brief_left
perplexity_only_params: [search_domain_filter, search_recency_filter]
batch_scale_gate_key: skills.personalize.ai_enrich_batch_min_rows
batch_scale_on_missing_key: stop
# Whatever comes back is ai_inferred. It is never mixed into verified fields and
# is never assertable as a claim, however high its confidence.
ai_enrich_output_provenance: ai_inferred
ai_enrich_output_assertable: false
malformed_response_storage: ai_inferred_invalid
The gate key this skill reads
gates.yaml:skills.personalize.ai_enrich_batch_min_rows is the row count above which
the batch-scale route to ai_enrich opens. Below it, drafting stays local, which is
the default and the cheap answer: the pack already runs inside a model that writes at
no marginal cost, so paying per row to draft is a billing error rather than a
capability.
If the key is ever absent, gateValue() throws MissingGateKey, the batch route
returns stop, and drafting stays local — the closed direction is also the correct
one here, which is a pleasant place to be. Read the value rather than quoting one:
richapi gates skills.personalize.ai_enrich_batch_min_rows
Everything else in the table above is structural and works today. The Iron Law does not depend on a threshold, and it never will: whether a brief carries a source line is not a number somebody tunes.
Writing the line
Once a claim is graded supported, the craft rules are ordinary and short.
- Lead with the fact, not with yourself. The first clause should be something only someone who read about them could write.
- One claim. Two is a dossier. Three is a background check.
- Say what it means for them, not what it means for you. The claim earns the right to the next sentence; the next sentence has to be about their problem.
- Short. A first line is one sentence. A body is three or four.
- No compliment as a substitute for research. Impressive background is banned precisely because it is what you write when you have nothing.
Write the draft to gtm/copy/<play>/<contact>.yaml, and keep the provenance with it:
the claim, the field it came from, the grade, and the source line. A draft without its
evidence trail cannot be re-checked when the user asks "where did we get that", which
is the question that arrives after the reply nobody wanted.
What a refusal looks like
Refusals are the output, not an error. Say them plainly and per contact:
40 contacts. 12 drafted with a supported claim. 28 refused: 19
not_found(the brief carries nothing to personalise on), 7not_verifiable(the fact is there but its only source is the model itself), 2not_applicable(the brief already recorded that the question does not apply). No draft was written for the 28.
Then offer the two honest routes, in this order:
- Go get the evidence.
/account-researchor the grounded path above will fetch what the brief is missing. This is the fix. - Send a claim-free opener. A short, honest, non-personalised line is a legitimate product. It converts worse than a real observation and far better than an invented one, and it is not a failure to offer it.
What you never do is close the gap yourself. Do not soften not_found into a sentence
that gestures at the fact without stating it. Do not write around it with something the
brief also does not support. Do not average two weak signals into one confident claim.
Every one of those is the same move: turning "we did not find out" into text a stranger
will read as fact.
What this skill will not do
- It will not write a claim the brief does not carry. That is the Iron Law and there is no flag, no override and no "just this once" that gets past it.
- It will not hedge instead of refusing. A softened claim is still a claim, and it is a worse one.
- It will not assert an
ai_inferredvalue. Web grounding produces a lead to go verify, not a sentence to send. Confidence is not provenance. - It will not upgrade an explicit null.
not_foundin the brief isnot_foundin the output. Re-asking until the answer changes is not research. - It will not call
ai_enrichto write. Drafting is local; the metered hop exists for web grounding and batch scale only, and both are named and costed first. - It will not draft for a contact it cannot screen against suppression, and it will not draft for one that is suppressed.
- It will not grade its own claims. Grading belongs to
/evidence-score, and a writer marking its own work is how the Iron Law gets talked around. - It will not decide whether contacting these people is lawful. That is
/comply, and a well-sourced email to someone with no lawful basis is still a violation. - It will not send, schedule, or write an export for a sending tool. Sending
execution, LinkedIn actions and dialing are external to this pack, permanently. The
export belongs to
/launchalone.
Related
- Grade the claims first:
/evidence-score. This skill asserts only what that one gradedsupported. - Turn drafts into a cadence:
/sequence-builder. It designs the steps and spacing and writes the merge-tag skeleton; the drafts this skill produced fill it. - Fetch the evidence the brief is missing:
/enrich-waterfallfor contact data,/account-researchfor account facts. - Permission before persuasion:
/comply. - Sign-off then the export:
/campaign-reviewand/launch. - Session start, routing and the closing receipt:
/richapi-gtm. - The one explicit null enum, and why the pack enforces the shape rather than trusting
it:
_lib/dual-contract.schema.json.
Other skills for the same job
Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.
- personalize-message by Othmane-Khadri · 317
- personalize-email by explorium-ai · 156
- outreach-personalize by emelia-io · 17
Need help setting it up?
This page tells you what personalize does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.
Book a call →The directory stays free. There is nothing gated behind this.