ConvertKit - v1.0.0
Conditional Logic
A feed's optional conditional_logic decides whether the feed fires for a given submission. null (or an empty object) means "always fire" — most feeds don't need conditions.
Structure
{
"match": "all",
"conditions": [
{ "field": "plan", "operator": "equals", "value": "pro" },
{ "field": "email", "operator": "contains", "value": "@" }
]
}
match—"all"(AND) or"any"(OR). Defaults to"all"if omitted.conditions— an array of{ field, operator, value }objects.
Each condition compares the submission's value for field (using the submission's slug, not the Kit destination) against value using operator.
Operators
| Operator | Meaning |
|---|---|
equals |
Exact string match after normalization. |
not_equals |
Inverse of equals. |
contains |
Substring check on the actual value. For array values, checks each item. |
not_contains |
Inverse of contains. |
is_empty |
Actual value is null, empty string (after trim), or empty array. |
is_not_empty |
Inverse of is_empty. |
The full canonical list also lives on ConditionalLogicEvaluator::OPERATORS — used by the feed store request to reject unknown operators at validation time.
Value normalization
Both sides of an equality comparison are normalized to strings before comparing:
null→""true→"1",false→"0"- Scalars →
(string) $value - Arrays/objects →
json_encode()
This lets you write {"field": "opt_in", "operator": "equals", "value": "1"} regardless of whether the submission stores the checkbox as true, "1", or 1.
For contains and not_contains, if the actual value is an array, each item is checked (both exact and substring). This handles multi-select fields cleanly.
Semantics
match: "all"— every condition must pass. If any condition throws or is malformed, the feed does not fire.match: "any"— at least one condition must pass. Malformed conditions are treated as failing.- Missing operator or field → the condition is treated as failing.
Missing conditions arrays or an empty conditions list → the feed fires (nothing to reject it).
Examples
Only fire for pro plans
{
"match": "all",
"conditions": [
{ "field": "plan", "operator": "equals", "value": "pro" }
]
}
Fire for anyone who selected either "marketing" or "sales" as their role
{
"match": "any",
"conditions": [
{ "field": "role", "operator": "equals", "value": "marketing" },
{ "field": "role", "operator": "equals", "value": "sales" }
]
}
Skip when the honeypot field was filled
{
"match": "all",
"conditions": [
{ "field": "hp_url", "operator": "is_empty" }
]
}
Only fire for @acme.com email addresses
{
"match": "all",
"conditions": [
{ "field": "email", "operator": "contains", "value": "@acme.com" }
]
}
Testing your rules
Dry-run the feed against representative submissions:
POST /admin/convertkit/feeds/{id}/test
{ "values": { "email": "x@y.co", "plan": "pro" } }
{ "would_send": true, "reason": null, "payload": {...} }— the rules passed.{ "would_send": false, "reason": "conditional_logic", "payload": null }— the rules failed.
See Feed Dry-Run.
Direct usage
Instantiate the evaluator directly if you want to reuse the same rule engine elsewhere:
use ArtisanPackUI\ConvertKit\Support\ConditionalLogicEvaluator;
$evaluator = app( ConditionalLogicEvaluator::class );
$passes = $evaluator->evaluate( $rules, $submissionValues );
Pure and dependency-free — no side effects, no HTTP calls.
Errors thrown from a rule
If a bad condition throws unexpectedly, the listener catches it and emits KitFeedSkipped with reason: 'evaluator_error:...'. This lets ops trace which feed misfired without silently losing the submission for other feeds.