Campaign Operations
Check a campaign audience before sending
Check a proposed audience against supplied consent and suppression facts before a campaign operator sends anything. The output names eligible recipient IDs and gives explicit suppression reasons.
Experimental native example · local preview · revision 1
WORKFLOW PREVIEW
See how it works
Use + and − to zoom, F to fit. Press Enter on a node to inspect it.
Private preview. These tested drafts are available for inspection. Adding them to a Vora workspace is still being tested.
What you need
- Current recipient-level consent and opt-out records from an authorized source.
- Unique recipient IDs, email addresses and an operator responsible for checking the records are current and applicable to the campaign.
Follow the workflow step by step
Supplied agency data
Receive the explicitly supplied manual input packet.
Check a campaign audience before sending
Return eligible recipient IDs and suppressed recipient IDs with explicit reasons from operator-supplied consent and suppression records. Send nothing.
Setup and review
- Supply campaignId and at most 500 recipients. Use explicit booleans for consent and optedOut.
- Test an eligible recipient, an opted-out recipient and one whose consent is absent.
- Inspect every suppressed reason before using the eligible IDs in a separately authorized campaign action.
Inspect every node
Expand a node to inspect its complete settings. Review the runtime and adoption evidence before installing it.
Supplied agency data · wfManualTrigger
{
"label": "Supplied agency data"
}Check a campaign audience before sending · code
{
"label": "Check a campaign audience before sending",
"code": "\nfunction obj(value, keys, required = keys) {\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected object');\n if (Object.keys(value).some(key => !keys.includes(key))) throw new Error('Unknown input field');\n if (required.some(key => !Object.hasOwn(value, key))) throw new Error('Missing required input field');\n return value;\n}\nfunction text(value, name, max = 200, allowEmpty = false) {\n if (typeof value !== 'string' || value.length > max || (!allowEmpty && !value.trim())) throw new Error('Invalid ' + name);\n return value.trim();\n}\nfunction list(value, name, max = 500) {\n if (!Array.isArray(value) || value.length > max) throw new Error('Invalid ' + name);\n return value;\n}\nfunction int(value, name, min = 0, max = Number.MAX_SAFE_INTEGER) {\n if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error('Invalid ' + name);\n return value;\n}\nfunction amount(value, name) {\n if (!Number.isFinite(value) || value < 0 || value > 1000000000) throw new Error('Invalid ' + name);\n return value;\n}\nfunction email(value) { return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value); }\nfunction day(value) {\n if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) throw new Error('Invalid calendar date');\n const ms = Date.parse(value + 'T00:00:00Z');\n if (!Number.isFinite(ms) || new Date(ms).toISOString().slice(0, 10) !== value) throw new Error('Invalid calendar date');\n return ms;\n}\nfunction instant(value) {\n if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z$/.test(value)) throw new Error('Invalid UTC timestamp');\n const ms = Date.parse(value);\n const normalized = value.includes('.') ? value : value.replace('Z', '.000Z');\n if (!Number.isFinite(ms) || new Date(ms).toISOString() !== normalized) throw new Error('Invalid UTC timestamp');\n return ms;\n}\nfunction uniqueIds(items) {\n const ids = items.map(item => text(item.id, 'item ID', 120));\n if (new Set(ids).size !== ids.length) throw new Error('Duplicate item ID');\n return ids;\n}\nfunction ratioBps(numerator, denominator) {\n const n = BigInt(numerator) * 10000n;\n const d = BigInt(denominator);\n const quotient = n / d;\n const floor = n < 0n && n % d !== 0n ? quotient - 1n : quotient;\n const result = Number(floor);\n if (!Number.isSafeInteger(result)) throw new Error('Unsafe calculated ratio');\n return result;\n}\n\n\nconst x = obj(context.input, ['campaignId','recipients']);\nconst campaignId = text(x.campaignId, 'campaign ID', 120);\nconst recipients = list(x.recipients, 'recipients');\nrecipients.forEach(item => obj(item, ['id','email','consent','optedOut'], ['id','email']));\nconst ids = uniqueIds(recipients);\nconst eligibleIds = [], suppressed = [];\nrecipients.forEach((item, index) => {\n if (Object.hasOwn(item, 'consent') && typeof item.consent !== 'boolean') throw new Error('Invalid consent flag');\n if (Object.hasOwn(item, 'optedOut') && typeof item.optedOut !== 'boolean') throw new Error('Invalid suppression flag');\n const address = text(item.email, 'email', 320, true).toLowerCase();\n const reasons = [];\n if (item.consent !== true) reasons.push('consent_not_confirmed');\n if (item.optedOut === true) reasons.push('opted_out');\n else if (item.optedOut !== false) reasons.push('suppression_not_confirmed');\n if (!email(address)) reasons.push('invalid_email');\n if (reasons.length) suppressed.push({id:ids[index], reasons});\n else eligibleIds.push(ids[index]);\n});\nreturn {campaignId, eligibleIds, suppressed};\n",
"outputVar": "result",
"timeout": 1000,
"memoryLimit": 16
}Input and output contracts
Input
{
"type": "object",
"properties": {
"campaignId": {
"type": "string",
"maxLength": 120
},
"recipients": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"maxLength": 120
},
"email": {
"type": "string",
"maxLength": 320
},
"consent": {
"type": "boolean"
},
"optedOut": {
"type": "boolean"
}
},
"required": [
"id",
"email"
],
"additionalProperties": false
},
"maxItems": 500
}
},
"required": [
"campaignId",
"recipients"
],
"additionalProperties": false
}Output
{
"type": "object",
"properties": {
"campaignId": {
"type": "string",
"maxLength": 120
},
"eligibleIds": {
"type": "array",
"items": {
"type": "string",
"maxLength": 120
},
"maxItems": 500
},
"suppressed": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"maxLength": 120
},
"reasons": {
"type": "array",
"items": {
"type": "string",
"maxLength": 200
},
"maxItems": 3
}
},
"required": [
"id",
"reasons"
],
"additionalProperties": false
},
"maxItems": 500
}
},
"required": [
"campaignId",
"eligibleIds",
"suppressed"
],
"additionalProperties": false
}Choose an output for this job
This business workflow also has 15 delivery formats. Compare their inputs, output examples and limits.
Example input and result
These examples use synthetic data.
Input
{
"campaignId": "cmp-01",
"recipients": [
{
"id": "r1",
"email": "yes@example.test",
"consent": true,
"optedOut": false
},
{
"id": "r2",
"email": "no@example.test",
"consent": true,
"optedOut": true
}
]
}Expected result
{
"campaignId": "cmp-01",
"eligibleIds": [
"r1"
],
"suppressed": [
{
"id": "r2",
"reasons": [
"opted_out"
]
}
]
}Success means: Return eligible recipient IDs and suppressed recipient IDs with explicit reasons from operator-supplied consent and suppression records. Send nothing.
When something goes wrong
- Duplicate IDs stop the batch because a decision could target the wrong record. Correct the IDs and repeat. Missing consent, unknown suppression state or invalid addresses suppress that recipient; recover only from a verified record update.
Limits and costs
- The result evaluates supplied facts. It is not a legal compliance opinion, a consent collection flow, inbox validation or a sent campaign. No address is contacted.
- No paid model calls or metered third-party operations are declared in this pure graph. Vora plan availability, compute and quotas still apply; pricing has not been verified by this batch.