Campaign Operations
Check a campaign audience before sending — ClickUp task draft
Return eligible recipient IDs and suppressed recipient IDs with explicit reasons from operator-supplied consent and suppression records. Send nothing. Prepare a ClickUp task draft.
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
- Authorized supplied job facts matching the original closed input contract.
- Explicit review metadata and target destination fields; no implicit team, channel, list or project selection.
Follow the workflow step by step
Supplied agency job and review metadata
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.
- default: ClickUp task draft
ClickUp task draft
Return eligible recipient IDs and suppressed recipient IDs with explicit reasons from operator-supplied consent and suppression records. Send nothing.
Setup and review
- Download the workflow JSON and inspect its jobInput and deliveryMetadata contracts.
- Supply the original agency job input plus explicit review metadata and destination.
- Run in an isolated test and inspect the complete jobResult, target status, holds and escape report before taking an external action.
Inspect every node
Expand a node to inspect its complete settings. Review the runtime and adoption evidence before installing it.
Supplied agency job and review metadata · wfManualTrigger
{
"label": "Supplied agency job and review metadata"
}Check a campaign audience before sending · code
{
"label": "Check a campaign audience before sending",
"code": "return (function(context){\n\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\n})({input:context.input.jobInput});\n",
"outputVar": "jobResult",
"timeout": 3000,
"memoryLimit": 64
}ClickUp task draft · code
{
"label": "ClickUp task draft",
"code": "const jobResult=context.jobResult;\nconst delivery=(function(context){\nreturn ((function(originalResult,metadata,destination){\nfunction stableResult(value,depth=0){\n if(depth>64)throw new Error('Result nesting exceeds 64');\n if(value===null||typeof value==='string'||typeof value==='boolean')return JSON.stringify(value);\n if(typeof value==='number'&&Number.isFinite(value))return JSON.stringify(value);\n if(Array.isArray(value)){\n if(Object.keys(value).length!==value.length||Array.from({length:value.length},(_,i)=>i).some(i=>!Object.hasOwn(value,i)))throw new Error('Result contains sparse data');\n return '['+value.map(item=>stableResult(item,depth+1)).join(',')+']';\n }\n if(value&&typeof value==='object')return '{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+stableResult(value[key],depth+1)).join(',')+'}';\n throw new Error('Result is not JSON data');\n}\nfunction utf8Length(text){\n let bytes=0;\n for(let i=0;i<text.length;i++){\n const code=text.charCodeAt(i);\n if(code<128)bytes++;else if(code<2048)bytes+=2;\n else if(code>=55296&&code<=56319&&i+1<text.length&&text.charCodeAt(i+1)>=56320&&text.charCodeAt(i+1)<=57343){bytes+=4;i++;}\n else bytes+=3;\n }\n return bytes;\n}\nfunction chunkText(text,maximum){\n const chunks=[];\n for(let start=0;start<text.length;){let end=Math.min(start+maximum,text.length);const last=text.charCodeAt(end-1),next=text.charCodeAt(end);if(end<text.length&&last>=55296&&last<=56319&&next>=56320&&next<=57343)end--;chunks.push(text.slice(start,end));start=end;}\n return chunks;\n}\nfunction deliveryState(rendererId,originalResult,metadata,destination,destinationKeys){\n const output={rendererId,status:'HELD',originalResult,drafts:[],holds:[],escapeReport:[]};\n const hold=(code,message)=>{output.holds.push({code,message});return {output,json:''};};\n const object=value=>value&&typeof value==='object'&&!Array.isArray(value);\n const exact=(value,keys)=>object(value)&&Object.keys(value).length===keys.length&&keys.every(key=>Object.hasOwn(value,key));\n const keys=['workflowId','canonicalIntentId','title','reviewUrl'],maxima=[128,200,200,1000];\n if(!exact(metadata,keys)||keys.some((key,i)=>typeof metadata[key]!=='string'||!metadata[key].length||metadata[key].length>maxima[i]||/[\\u0000-\\u001f\\u007f]/.test(metadata[key]))||!/^https?:\\/\\/[^\\s<>\"']+$/.test(metadata.reviewUrl))return hold('INVALID_METADATA','Metadata must provide bounded workflowId, canonicalIntentId, title and an HTTP(S) reviewUrl.');\n if(!exact(destination,destinationKeys)||destinationKeys.some(key=>typeof destination[key]!=='string'||!destination[key].length||destination[key].length>200||destination[key]!==destination[key].trim()||/[\\u0000-\\u001f\\u007f]/.test(destination[key])))return hold('INVALID_DESTINATION','Destination fields must be exactly: '+destinationKeys.join(', ')+'.');\n let json;\n try{if(!object(originalResult))throw new Error('Primary result must be a JSON object');json=stableResult(originalResult);}catch(error){return hold('INVALID_RESULT',error.message);}\n if(json.length>32768||utf8Length(json)>65536)return hold('RESULT_BOUND_EXCEEDED','Original result exceeds this renderer profile; full original result is retained.');\n return {output,json};\n}\nfunction escapeHtml(text){return text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"').replace(/'/g,''');}\nfunction escapeMarkdown(text){return text.replace(/&/g,'&').replace(/[\\\\`*{}\\[\\]()#+\\-.!_|>~<]/g,'\\\\$&');}\nfunction markdownReviewText(metadata,json){\n const runs=json.match(/`+/g)||[],fence='`'.repeat(Math.max(3,...runs.map(run=>run.length+1)));\n return '# '+escapeMarkdown(metadata.title)+'\\n\\nWorkflow: '+escapeMarkdown(metadata.workflowId)+'\\nCanonical job: '+escapeMarkdown(metadata.canonicalIntentId)+'\\nReview: <'+metadata.reviewUrl.replace(/&/g,'&')+'>\\n\\n'+fence+'json\\n'+json+'\\n'+fence+'\\n';\n}\nfunction plainReviewText(metadata,json){return metadata.title+'\\nWorkflow: '+metadata.workflowId+'\\nCanonical job: '+metadata.canonicalIntentId+'\\nReview: '+metadata.reviewUrl+'\\n\\n'+json;}\nfunction finishDraft(state,draft,texts=[],textMaximum=32768,byteMaximum=131072){\n if(texts.some(text=>text.length>textMaximum)||utf8Length(JSON.stringify(draft))>byteMaximum){state.output.holds.push({code:'TARGET_BOUND_EXCEEDED',message:'Draft exceeds the target or Vora profile bound; no draft is emitted and the full original result is retained.'});return state.output;}\n state.output.status='PREPARED';state.output.drafts=[draft];return state.output;\n}\nreturn (function clickupTaskReview(originalResult,metadata,destination){\n const state=deliveryState('clickup-task-review',originalResult,metadata,destination,['listId']);if(state.output.holds.length)return state.output;\n const description=plainReviewText(metadata,state.json);return finishDraft(state,{listId:destination.listId,body:{name:metadata.title,description}},[description]);\n})(originalResult,metadata,destination);\n}))(context.input.jobResult,context.input.deliveryMetadata.metadata,context.input.deliveryMetadata.destination);\n\n})({input:{jobResult,deliveryMetadata:context.input.deliveryMetadata}});\nreturn {jobResult,delivery};\n",
"outputVar": "result",
"timeout": 3000,
"memoryLimit": 64
}Input and output contracts
Input
{
"type": "object",
"properties": {
"jobInput": {
"additionalProperties": false,
"properties": {
"campaignId": {
"maxLength": 120,
"type": "string"
},
"recipients": {
"items": {
"additionalProperties": false,
"properties": {
"consent": {
"type": "boolean"
},
"email": {
"maxLength": 320,
"type": "string"
},
"id": {
"maxLength": 120,
"type": "string"
},
"optedOut": {
"type": "boolean"
}
},
"required": [
"id",
"email"
],
"type": "object"
},
"maxItems": 500,
"type": "array"
}
},
"required": [
"campaignId",
"recipients"
],
"type": "object"
},
"deliveryMetadata": {
"type": "object",
"properties": {
"metadata": {
"type": "object",
"properties": {
"workflowId": {
"type": "string",
"maxLength": 128
},
"canonicalIntentId": {
"type": "string",
"maxLength": 200
},
"title": {
"type": "string",
"maxLength": 200
},
"reviewUrl": {
"type": "string",
"maxLength": 1000
}
},
"required": [
"workflowId",
"canonicalIntentId",
"title",
"reviewUrl"
],
"additionalProperties": false
},
"destination": {
"type": "object",
"properties": {
"listId": {
"type": "string",
"maxLength": 200
}
},
"required": [
"listId"
],
"additionalProperties": false
}
},
"required": [
"metadata",
"destination"
],
"additionalProperties": false
}
},
"required": [
"jobInput",
"deliveryMetadata"
],
"additionalProperties": false
}Output
{
"type": "object",
"properties": {
"jobResult": {
"additionalProperties": false,
"properties": {
"campaignId": {
"maxLength": 120,
"type": "string"
},
"eligibleIds": {
"items": {
"maxLength": 120,
"type": "string"
},
"maxItems": 500,
"type": "array"
},
"suppressed": {
"items": {
"additionalProperties": false,
"properties": {
"id": {
"maxLength": 120,
"type": "string"
},
"reasons": {
"items": {
"maxLength": 200,
"type": "string"
},
"maxItems": 3,
"type": "array"
}
},
"required": [
"id",
"reasons"
],
"type": "object"
},
"maxItems": 500,
"type": "array"
}
},
"required": [
"campaignId",
"eligibleIds",
"suppressed"
],
"type": "object"
},
"delivery": {
"type": "object",
"properties": {
"rendererId": {
"type": "string",
"maxLength": 128,
"enum": [
"clickup-task-review"
]
},
"status": {
"type": "string",
"maxLength": 16,
"enum": [
"PREPARED",
"HELD"
]
},
"originalResult": {
"additionalProperties": false,
"properties": {
"campaignId": {
"maxLength": 120,
"type": "string"
},
"eligibleIds": {
"items": {
"maxLength": 120,
"type": "string"
},
"maxItems": 500,
"type": "array"
},
"suppressed": {
"items": {
"additionalProperties": false,
"properties": {
"id": {
"maxLength": 120,
"type": "string"
},
"reasons": {
"items": {
"maxLength": 200,
"type": "string"
},
"maxItems": 3,
"type": "array"
}
},
"required": [
"id",
"reasons"
],
"type": "object"
},
"maxItems": 500,
"type": "array"
}
},
"required": [
"campaignId",
"eligibleIds",
"suppressed"
],
"type": "object"
},
"drafts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"listId": {
"type": "string",
"maxLength": 200
},
"body": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 32768
}
},
"required": [
"name",
"description"
],
"additionalProperties": false
}
},
"required": [
"listId",
"body"
],
"additionalProperties": false
},
"maxItems": 1,
"minItems": 0
},
"holds": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": {
"type": "string",
"maxLength": 128
},
"message": {
"type": "string",
"maxLength": 2000
}
},
"required": [
"code",
"message"
],
"additionalProperties": false
},
"maxItems": 10,
"minItems": 0
},
"escapeReport": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"maxLength": 256
},
"method": {
"type": "string",
"maxLength": 80
},
"originalValue": {
"type": "string",
"maxLength": 32768
},
"escapedValue": {
"type": "string",
"maxLength": 32768
}
},
"required": [
"field",
"method",
"originalValue",
"escapedValue"
],
"additionalProperties": false
},
"maxItems": 1000,
"minItems": 0
}
},
"required": [
"rendererId",
"status",
"originalResult",
"drafts",
"holds",
"escapeReport"
],
"additionalProperties": false
}
},
"required": [
"jobResult",
"delivery"
],
"additionalProperties": false
}Related implementations
Compare implementations for this job · Primary business workflow
Caller supplies listId for the list task endpoint. Body contains name and complete plain-text description. The renderer emits description, without markdown_content or invented assignees/custom fields. Original result, including original reasons/holds, is preserved without mutation. Missing/extra destination fields or invalid metadata hold the draft. No truncation: overflow returns HELD with an empty drafts array and full original result.
Supplied input: vora-agency-campaign-consent-preflight · 1. Draft/result: clickup-task-review · ClickUp REST API v2 list task.
Mapping and loss rules
- Caller supplies listId for the list task endpoint. Body contains name and complete plain-text description.
- The renderer emits description, without markdown_content or invented assignees/custom fields.
- Original result, including original reasons/holds, is preserved without mutation.
- Missing/extra destination fields or invalid metadata hold the draft.
- No truncation: overflow returns HELD with an empty drafts array and full original result.
This binding consumes supplied data. It does not establish an authenticated connector or a provider write.
Example input and result
These examples use synthetic data.
Input
{
"jobInput": {
"campaignId": "cmp-01",
"recipients": [
{
"id": "r1",
"email": "yes@example.test",
"consent": true,
"optedOut": false
},
{
"id": "r2",
"email": "no@example.test",
"consent": true,
"optedOut": true
}
]
},
"deliveryMetadata": {
"metadata": {
"workflowId": "agency-campaign-consent-preflight",
"canonicalIntentId": "campaign.evaluate-recipient-permission",
"title": "Check a campaign audience before sending",
"reviewUrl": "https://example.test/workflows/campaign-consent-preflight/"
},
"destination": {
"listId": "901"
}
}
}Expected result
{
"jobResult": {
"campaignId": "cmp-01",
"eligibleIds": [
"r1"
],
"suppressed": [
{
"id": "r2",
"reasons": [
"opted_out"
]
}
]
},
"delivery": {
"rendererId": "clickup-task-review",
"status": "PREPARED",
"originalResult": {
"campaignId": "cmp-01",
"eligibleIds": [
"r1"
],
"suppressed": [
{
"id": "r2",
"reasons": [
"opted_out"
]
}
]
},
"drafts": [
{
"listId": "901",
"body": {
"name": "Check a campaign audience before sending",
"description": "Check a campaign audience before sending\nWorkflow: agency-campaign-consent-preflight\nCanonical job: campaign.evaluate-recipient-permission\nReview: https://example.test/workflows/campaign-consent-preflight/\n\n{\"campaignId\":\"cmp-01\",\"eligibleIds\":[\"r1\"],\"suppressed\":[{\"id\":\"r2\",\"reasons\":[\"opted_out\"]}]}"
}
}
],
"holds": [],
"escapeReport": []
}
}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
- Review original business holds and target holds separately. Correct the relevant supplied input and rerun without discarding the original job result.
Limits and costs
- This prepares a target-specific review draft from the original agency decision. The complete original job result and business holds remain visible. A held target produces no draft. No provider request, send, import, issue creation or publication occurs.
- 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.