Reporting

Campaign verdict

Read one Outreach Hub campaign's test numbers against its own kill and scale rules and recommend scale, keep testing or kill, or say there is not enough data yet, in one sentence that quotes the numbers behind it. Also says which rule lines were crossed and which could not be judged. Use each morning and whenever a test's numbers change. Recommends only: never changes a stage, sends, spends or makes the call, which is always a person's.

DraftNot used in a campaign yetVersion 2026.09.29.1

campaign-verdict/SKILL.md12.7 KB314 lines

When asked which version of this skill is active, report metadata.version exactly.

Answer only with JSON that matches schemas/verdict.schema.json.

You read one campaign's numbers and its kill and scale rules, and you recommend what a person should do next. The desk shows your answer on the campaign as Claude's call; a person decides. Never write as if anything has happened or will happen because of your answer.

What you are given

A JSON object matching schemas/verdict-input.schema.json:

  • campaign: name and channels (email, call or both).
  • rules: this campaign's own rules, as campaign-planner wrote them. Rates are fractions: 0.015 is 1.5%. They are usually the house rules in campaign-planner's SKILL.md, but always judge by the ones you are given.
  • bounce_min_sends: sends before the bounce line is judged. It is the same floor the campaign card uses before it flags a bounce rate.
  • numbers: sent, bounced, positive_replies, dials, connects, meetings.

Null is unknown, never zero

A count is null when the desk has no source for it yet or has not synced it. Never read null as 0. A line that needs a null number is unknown, and an unknown line can never cross a bar. "No positive replies yet" and "positive replies not counted yet" are different sentences; only the first may come from a 0.

Smartlead's reply count is not in the input on purpose: it includes out-of-office and no-thanks replies. Never estimate positive replies from anything else you are given.

The lines

Judge only the lines for the campaign's channels. Set the others to null in checks. meetings_scale always applies.

CheckChannelSampleRateCrossed when
positive_killemailsent of at least minSendspositive_replies / sentrate is under positiveRate.kill
positive_scaleemailsent of at least minSendspositive_replies / sentrate is over positiveRate.scale
connect_killcalldials of at least minDialsconnects / dialsrate is under connectRate.kill
connect_scalecalldials of at least minDialsconnects / dialsrate is over connectRate.scale
bounce_killemailsent of at least bounce_min_sendsbounced / sentrate is over maxBounceRate
meetings_scaleanynonenonemeetings is at least scaleMeetings

"Under" and "over" are strict: a rate exactly on a line has not crossed it. Compare the exact fraction, never a rounded one.

Give each line a status, deciding in this order:

  1. unknown when the sample count (sent or dials) is null.
  2. under_minimum when the sample count is below the line's minimum.
  3. unknown when the other count the line needs is null.
  4. crossed or not_crossed.

observed is what the line was judged on, as a person writes it: "0.4% after 612 sends", "3.9% after 155 dials", "312 of 400 sends", "1 meeting". It is null only when the status is unknown.

The verdict

Take the first that applies:

  1. Bounce kill beats everything. bounce_kill crossed: kill, even when replies or meetings would scale. A list that bounces burns the sending domains for every campaign.
  2. Meetings clear the scale bar on their own. meetings_scale crossed: scale, whatever the sample sizes, and even if a channel crossed its kill line. Meetings are what the test is for.
  3. A channel that scales wins. positive_scale or connect_scale crossed: scale. When the other channel crossed its kill line, still scale, and the reason says which channel to scale and which to stop. The audience answered; the failing channel is the thing to drop.
  4. Kill only when every channel says kill. Every channel the campaign uses has crossed its kill line (positive_kill for email, connect_kill for calls): kill. A kill on one channel while the other is still under its minimum or unknown is keep_testing, so an unfinished channel is never killed for the other's result.
  5. A reply or connect line was judged. Any of positive_kill, positive_scale, connect_kill or connect_scale is crossed or not_crossed: keep_testing.
  6. Otherwise not_enough_data. A bounce rate under its line, or fewer meetings than the bar, does not say whether the audience answers, so neither makes a test judged on its own.

scale and kill mean a bar was crossed and the test is waiting on a person. keep_testing and not_enough_data mean it is not, yet.

The reason

One sentence, under 240 characters, that a teammate can check against the numbers. Quote the numbers that drove the verdict and the line they crossed or missed. Write rates as the desk's rule lines do: a percentage with at most one decimal and no trailing ".0" ("0.4%", "6%", "11.7%"). When one decimal would make a rate look equal to a line it did not equal, use two ("0.49%"). Write counts with commas ("1,204 sends").

  • not_enough_data: say what is missing: how far the sample is from its minimum ("312 of 400 sends"), or which numbers are not counted yet.
  • keep_testing: say where the rate sits between the lines.
  • A split (rule 3): name both channels and both rates.

Plain English, active voice, no em dashes. Never promise results, never say the campaign was scaled or killed, and never name a prospect.

Examples

The campaigns and numbers below are illustrations, not real results. Every example uses the house rules: {"minSends": 400, "minDials": 150, "positiveRate": {"kill": 0.005, "scale": 0.015}, "connectRate": {"kill": 0.05, "scale": 0.1}, "maxBounceRate": 0.04, "scaleMeetings": 2} and bounce_min_sends 100.

Not enough data: counts the desk does not have yet

Input:

{
  "campaign": { "name": "Example email and calls test", "channels": ["email", "call"] },
  "rules": { "minSends": 400, "minDials": 150, "positiveRate": { "kill": 0.005, "scale": 0.015 }, "connectRate": { "kill": 0.05, "scale": 0.1 }, "maxBounceRate": 0.04, "scaleMeetings": 2 },
  "bounce_min_sends": 100,
  "numbers": { "sent": 612, "bounced": 11, "positive_replies": null, "dials": null, "connects": null, "meetings": null }
}

Answer:

{
  "verdict": "not_enough_data",
  "reason": "Bounce rate is fine at 1.8% after 612 sends, but positive replies, dials and meetings are not counted yet, so there is not enough data to judge.",
  "checks": {
    "positive_kill": { "status": "unknown", "observed": null },
    "connect_kill": { "status": "unknown", "observed": null },
    "bounce_kill": { "status": "not_crossed", "observed": "1.8% after 612 sends" },
    "positive_scale": { "status": "unknown", "observed": null },
    "connect_scale": { "status": "unknown", "observed": null },
    "meetings_scale": { "status": "unknown", "observed": null }
  }
}

Kill on bounce rate, whatever else

Input:

{
  "campaign": { "name": "Example email test", "channels": ["email"] },
  "rules": { "minSends": 400, "minDials": 150, "positiveRate": { "kill": 0.005, "scale": 0.015 }, "connectRate": { "kill": 0.05, "scale": 0.1 }, "maxBounceRate": 0.04, "scaleMeetings": 2 },
  "bounce_min_sends": 100,
  "numbers": { "sent": 450, "bounced": 27, "positive_replies": 9, "dials": null, "connects": null, "meetings": 2 }
}

Answer:

{
  "verdict": "kill",
  "reason": "Bounce rate is 6% after 450 sends, over the 4% kill line, which outweighs 2% positive reply and 2 meetings booked.",
  "checks": {
    "positive_kill": { "status": "not_crossed", "observed": "2% after 450 sends" },
    "connect_kill": null,
    "bounce_kill": { "status": "crossed", "observed": "6% after 450 sends" },
    "positive_scale": { "status": "crossed", "observed": "2% after 450 sends" },
    "connect_scale": null,
    "meetings_scale": { "status": "crossed", "observed": "2 meetings" }
  }
}

Scale on both channels

Input:

{
  "campaign": { "name": "Example email and calls test", "channels": ["email", "call"] },
  "rules": { "minSends": 400, "minDials": 150, "positiveRate": { "kill": 0.005, "scale": 0.015 }, "connectRate": { "kill": 0.05, "scale": 0.1 }, "maxBounceRate": 0.04, "scaleMeetings": 2 },
  "bounce_min_sends": 100,
  "numbers": { "sent": 480, "bounced": 8, "positive_replies": 9, "dials": 162, "connects": 19, "meetings": 1 }
}

Answer:

{
  "verdict": "scale",
  "reason": "1.9% positive reply after 480 sends and 11.7% connect rate after 162 dials both clear their scale lines of 1.5% and 10%.",
  "checks": {
    "positive_kill": { "status": "not_crossed", "observed": "1.9% after 480 sends" },
    "connect_kill": { "status": "not_crossed", "observed": "11.7% after 162 dials" },
    "bounce_kill": { "status": "not_crossed", "observed": "1.7% after 480 sends" },
    "positive_scale": { "status": "crossed", "observed": "1.9% after 480 sends" },
    "connect_scale": { "status": "crossed", "observed": "11.7% after 162 dials" },
    "meetings_scale": { "status": "not_crossed", "observed": "1 meeting" }
  }
}

Split: email scales, calls cross the kill line

Input:

{
  "campaign": { "name": "Example email and calls test", "channels": ["email", "call"] },
  "rules": { "minSends": 400, "minDials": 150, "positiveRate": { "kill": 0.005, "scale": 0.015 }, "connectRate": { "kill": 0.05, "scale": 0.1 }, "maxBounceRate": 0.04, "scaleMeetings": 2 },
  "bounce_min_sends": 100,
  "numbers": { "sent": 420, "bounced": 5, "positive_replies": 7, "dials": 155, "connects": 6, "meetings": 0 }
}

Answer:

{
  "verdict": "scale",
  "reason": "Scale on email at 1.7% positive reply after 420 sends, and stop the calls, which are at a 3.9% connect rate after 155 dials, under the 5% kill line.",
  "checks": {
    "positive_kill": { "status": "not_crossed", "observed": "1.7% after 420 sends" },
    "connect_kill": { "status": "crossed", "observed": "3.9% after 155 dials" },
    "bounce_kill": { "status": "not_crossed", "observed": "1.2% after 420 sends" },
    "positive_scale": { "status": "crossed", "observed": "1.7% after 420 sends" },
    "connect_scale": { "status": "not_crossed", "observed": "3.9% after 155 dials" },
    "meetings_scale": { "status": "not_crossed", "observed": "0 meetings" }
  }
}

Meetings clear the bar before the minimum

Input:

{
  "campaign": { "name": "Example calls test", "channels": ["call"] },
  "rules": { "minSends": 400, "minDials": 150, "positiveRate": { "kill": 0.005, "scale": 0.015 }, "connectRate": { "kill": 0.05, "scale": 0.1 }, "maxBounceRate": 0.04, "scaleMeetings": 2 },
  "bounce_min_sends": 100,
  "numbers": { "sent": null, "bounced": null, "positive_replies": null, "dials": 96, "connects": 7, "meetings": 2 }
}

Answer:

{
  "verdict": "scale",
  "reason": "2 meetings booked from the test batch clear the scale bar on their own, though calls have only 96 of 150 dials.",
  "checks": {
    "positive_kill": null,
    "connect_kill": { "status": "under_minimum", "observed": "96 of 150 dials" },
    "bounce_kill": null,
    "positive_scale": null,
    "connect_scale": { "status": "under_minimum", "observed": "96 of 150 dials" },
    "meetings_scale": { "status": "crossed", "observed": "2 meetings" }
  }
}

Keep testing: between the lines

Input:

{
  "campaign": { "name": "Example email test", "channels": ["email"] },
  "rules": { "minSends": 400, "minDials": 150, "positiveRate": { "kill": 0.005, "scale": 0.015 }, "connectRate": { "kill": 0.05, "scale": 0.1 }, "maxBounceRate": 0.04, "scaleMeetings": 2 },
  "bounce_min_sends": 100,
  "numbers": { "sent": 505, "bounced": 10, "positive_replies": 4, "dials": null, "connects": null, "meetings": 0 }
}

Answer:

{
  "verdict": "keep_testing",
  "reason": "0.8% positive reply after 505 sends sits between the 0.5% kill line and the 1.5% scale line, with no meetings yet.",
  "checks": {
    "positive_kill": { "status": "not_crossed", "observed": "0.8% after 505 sends" },
    "connect_kill": null,
    "bounce_kill": { "status": "not_crossed", "observed": "2% after 505 sends" },
    "positive_scale": { "status": "not_crossed", "observed": "0.8% after 505 sends" },
    "connect_scale": null,
    "meetings_scale": { "status": "not_crossed", "observed": "0 meetings" }
  }
}