Engine Core - Computation, Overrides & Scripting
The core engine that powers Campaign Manager - computation graph, dependency resolution, override system, scripting
1. The Computation Engine
This is the heart of Campaign Manager. When you trigger a computation, here is what happens internally:
Full Computation Flowchart
flowchart TD
Trigger["User triggers computation"] --> Load["Load all data from DB:
campaign -> phases -> steps -> CC items + env vars"]
Load --> BuildGraph["Build dependency graph
(analyze foreign keys)"]
BuildGraph --> ResolveEnv["Compute environment variables
(in order: strings, JSON, functions)"]
ResolveEnv --> TopoSort["Determine execution order
(topological sort by dependencies)"]
TopoSort --> ThreadPool["Allocate thread pool
(up to 30 threads per campaign)"]
ThreadPool --> Execute{"Execute each variable"}
Execute -->|AI prompt| LLM["Call LLM provider
(rate-limited, with retries)"]
Execute -->|Function| JS["Execute JavaScript
(with full context)"]
Execute -->|Asset| Render["Render document template
+ store file to S3"]
Execute -->|Static| Direct["Use stored value"]
LLM --> Store["Store result, update state"]
JS --> Store
Render --> Store
Direct --> Store
Store --> WS["Send WebSocket update
(real-time progress)"]
WS --> Next{"More variables?"}
Next -->|Yes| Execute
Next -->|No| Done["Computation complete"]
Done --> AutoDeploy{"Auto-deploy configured?"}
AutoDeploy -->|Yes| Deploy["Deploy assets to microsite"]
AutoDeploy -->|No| Finish["Done"]
Dependency Resolution - How Variables Connect
Each variable declares what other variables it depends on (its "foreign keys"). The system supports three dependency styles:
| Style | Example | How it works |
|---|---|---|
| No dependencies declared | (null foreign keys) | Depends on ALL previous items in sequence - implicit dependency |
| Simple key reference | headline | Searches backwards through the execution order for a variable with key "headline" |
| Path-based reference | Phase1/Step2/headline | Finds the variable at the exact path. Supports relative paths like ../headline |
Gotcha: Broken references don't always fail immediately
If a variable references a key that doesn't exist, the system marks it as having "broken references" and logs a warning. With the "Continue on error" strategy, computation proceeds and only this variable errors. With "Stop on error", the entire computation halts.
Computation Scopes
| Scope | What gets computed |
|---|---|
| Full campaign | Every phase, every step, every variable |
| Phase | All steps in the selected phase only |
| Step | All variables in the selected step only |
| Single variable | Just one variable + its downstream dependents |
| Custom scope | A user-selected set of variables |
Computation Modes
| Mode | What gets computed |
|---|---|
| Full | Force-recompute every variable regardless of current state |
| Not Computed Only | Only variables that haven't been computed yet |
| Not Computed + Assets | Uncomputed variables PLUS all asset variables (assets always regenerated) |
Downstream Propagation
When you recompute a single variable, you choose how far the effects ripple:
| Propagation | What gets recomputed |
|---|---|
| Me only | Just this variable - nothing else changes |
| Step downstream | This variable + everything that depends on it within the same step |
| Phase downstream | This variable + all dependents across the entire phase |
| Campaign downstream | Full cascade across the entire campaign |
Reset Options (How to Handle Existing Results)
| Option | What it does | Manual overrides |
|---|---|---|
| Reset all (status only) | Clear states -> "created", keep values | Kept |
| Reset all | Clear states AND values | Cleared |
| Skip overridden (stop hierarchy) | Don't recompute variables with overrides, stop cascading at overridden items | Kept, cascade stops |
| Skip overridden (with hierarchy) | Don't recompute variables with overrides, but continue cascading through them | Kept, cascade continues |
Error Handling During Computation
| Strategy | What happens when a variable fails |
|---|---|
| Continue with error | Mark this variable as "error", continue processing the rest |
| Stop on error | Halt the entire computation immediately, all threads stop |
Thread Pool Management
Dynamic Thread Allocation
The system starts with 1 thread per campaign and dynamically scales up to 30 threads based on workload. The total across all active campaigns is capped at 150 threads. Thread pool sizes are redistributed every 5 seconds among running computations. This means large campaigns with many AI prompts get more threads automatically.
Auto-Publishing After Computation
| Publishing Stage | When assets are deployed |
|---|---|
| No deploy | Never auto-deployed |
| On demand | Each asset deployed immediately after it's computed (one by one) |
| On execution done (all) | All assets deployed after full computation completes |
| On execution done (not generated) | Only non-generated assets deployed after computation |
Asset Generation Pipeline (7 Steps)
When an asset-type variable is computed, here is what happens:
- The system loads the associated document template (XSL, FreeMarker, or static HTML)
- It collects all computed variable values from the step as template context
- The template is rendered into a final document (HTML, PDF, etc.)
- The generated file is stored in the company's public S3 storage
- If the output is HTML, a PDF version is also generated automatically
- An Asset record is created with the file reference and deployment path
- Based on the publishing stage, the asset may be auto-deployed to the microsite
Real-Time WebSocket Updates
During computation, the system sends real-time events via WebSocket:
| Event | What it reports |
|---|---|
| Computation Status | Overall progress: total items, active, on-hold, finished/error |
| Item Updated | A specific variable finished computing - result available |
| Item Error | A variable failed - error message |
| Asset Published/Unpublished | An asset was deployed to or removed from the microsite |
| Post-Processing Result | Intermediate result from post-processing chain |
| Patched Item | A variable received a patched result |
| Log Event | Warning or info message (e.g., broken reference detected) |
The Customer App polls computation status every 2 seconds and auto-refreshes step data when computation completes.
Startup Recovery
Automatic Recovery After Restart
If the system restarts during an active computation, all "in-progress" variables are automatically reset to "created" state. No data is lost, but you need to re-trigger the computation.
2. Override & Precedence System
One of the most complex and least visible aspects of the system. Understanding who overrides whom is critical for product decisions.
CC Item Value Hierarchy
Every CC Item can have up to three values. The resultType field determines which value is returned:
flowchart TD
Compute["AI/Function computes value"] --> Result["result
(original computed value)"]
User["User manually overrides"] --> ResultOvr["resultOvr
(user override)"]
AI2["AI merges original + override"] --> Patched["patched
(AI-merged version)"]
Result --> Decision{"resultType?"}
ResultOvr --> Decision
Patched --> Decision
Decision -->|ORIGINAL| UseResult["Return: result"]
Decision -->|OVERRIDE| UseOvr["Return: resultOvr
(if exists, else result)"]
Decision -->|PATCHED| UsePatched["Return: patched"]
style UsePatched fill:#EEEEFD,stroke:#623CEA
style UseOvr fill:#F4FBFF,stroke:#22B5FF
style UseResult fill:#F7F7F7,stroke:#D4DAE0
| Value | Source | When it wins | Priority |
|---|---|---|---|
| patched | AI merges original + user override into a combined version | resultType = PATCHED | Highest |
| resultOvr | User manually entered a value that replaces the AI output | resultType = OVERRIDE | Medium |
| result | Original computed value (AI or function output) | resultType = ORIGINAL (default) | Lowest |
How patching works
When a variable has both an original AI result AND a user override, and a patch template is configured, the system can ask the AI to intelligently merge both versions. The AI receives the original and the override, and produces a "patched" version that incorporates both. This is useful when you want to keep the AI's structure but inject specific user changes.
Override protects against recomputation
When a variable has resultType = OVERRIDE and a valid override value, the system skips recomputing it entirely. It also respects the "Skip overridden" reset option - meaning downstream resets can stop at overridden items. This is how users protect their manual edits from being wiped by recomputation.
Chat Override Flow
sequenceDiagram
participant User
participant Chat as Chat Interface
participant System
participant CCItem as CC Item (Variable)
User->>Chat: Asks AI to suggest values
Chat->>System: AI generates suggestions
System->>System: Create ChatOverride (status: READY)
System-->>Chat: Show suggestions to user
alt User accepts
User->>Chat: Clicks "Apply"
Chat->>System: Set ChatOverride status = APPLIED
System->>CCItem: Set resultOvr = suggestion value
System->>CCItem: Set resultType = OVERRIDE
Note over CCItem: Variable now returns
user-approved AI suggestion
else User rejects
User->>Chat: Clicks "Cancel"
Chat->>System: Set ChatOverride status = CANCELED
Note over CCItem: Variable unchanged
end
Chat overrides are the bridge between conversational AI and campaign data. The key insight: the AI suggestion doesn't change anything until the user explicitly applies it. This is a safety mechanism.
AI Model Resolution Chain
flowchart LR
V["Variable-specific
model"] -->|if null| E["Env var prompt
model"]
E -->|if null| C["Campaign default
model"]
C -->|if null| S["System default
model"]
S -->|if null| Err["Error:
No model found"]
style V fill:#EEEEFD,stroke:#623CEA
style Err fill:#FFF2F2,stroke:#DC2626
Each level is optional. The system walks the chain until it finds a model. If nothing is configured at any level, computation fails with "AI model not defined".
Downstream Reset & Override Protection
When the system resets variables for recomputation, it respects overrides:
| Reset Option | Overridden variable | Variables below it |
|---|---|---|
| Reset all | Cleared (override removed) | All cleared |
| Reset all (status only) | State reset, override kept | All reset |
| Skip overridden (stop) | Skipped entirely | Also skipped (cascade stops) |
| Skip overridden (continue) | Skipped | Continue resetting below |
3. Scripting Engine
Function-type CC items and environment variables execute JavaScript (Nashorn engine, ECMAScript 5.1). Scripts have access to the full computation context through injected wrapper objects.
Available API Objects
llmApi - AI Model Access
| Method | What it does |
|---|---|
request(campaignId, promptKey, arg1, arg2, ...) | Single LLM call. Loads prompt template from env var, substitutes %s placeholders with arguments, sends to AI model. |
requestWithRetry(campaignId, promptKey, retries, arg1, ...) | Same as request() but retries on failure up to N times. |
request(campaignId, promptKey, jsonSchemaKey, arg1, ...) | LLM call with JSON schema constraint. Schema loaded from env var. AI output validated against JSON Schema v4. |
requestWithRetry(campaignId, promptKey, jsonSchemaKey, retries, arg1, ...) | JSON-schema-constrained call with retries. |
agentsApi - AI Agents (A2A)
| Method | What it does |
|---|---|
ask(agentName, campaignId, message) | Single-shot call to an A2A agent. Creates new conversation, sends message, returns response. Agents: execution_inspector, campaign_planner, writing_orchestrator, writing_manager. |
session(agentName, campaignId) | Creates a persistent multi-turn session with an agent. Returns an AgentSession object that maintains conversation state (contextId) between calls. |
AgentSession usage:
var session = agentsApi.session("writing_orchestrator", campaignId);
var first = session.send("Write a blog post about AI");
var refined = session.send("Make it shorter and add examples");
// Session remembers previous messages
campaignOpsApi - Campaign Operations
Query methods (read-only)
| Method | What it does |
|---|---|
listMyCompanies() | All companies accessible to current user |
listCampaigns(companyId) | All campaigns in a company |
listTemplates(companyId) | Published templates in a company |
listPhases(campaignId) | All phases in a campaign |
listItems(stepId) | All CC items in a step, ordered by sequence |
findPhasesByClasses(campaignId, classes[]) | Find phases with specific class tags |
findStepsByClass(campaignId, classNames[]) | Find steps with specific classes |
findCCItemInPhase(phaseId, key) | Find all items with a key in a phase |
externalKey(companyName, campaignName, path) | Access items from OTHER campaigns by name (cross-campaign reference) |
findItemByPath(campaignId, "Phase/Step/Key") | Find item by full path |
Mutation methods (write operations)
| Method | What it does |
|---|---|
startNewCampaign(templateId, companyId, name, btTitle) | Create campaign from template (deep clone) |
copyCampaign(sourceCampaignId, name, btTitle) | Clone campaign within same company |
updateCampaignKey(campaignId, path, content, btTitle) | Update a CC item value + reset downstream dependents |
updateCampaignKey(campaignId, path, content, resetDownstream, btTitle) | Same with optional downstream reset control |
updateCampaignKeys(campaignId, {path: value, ...}, btTitle) | Bulk update multiple items atomically + cascade reset |
updateCampaignKeysAndCompute(campaignId, {path: value}, upToPath, callback, btTitle) | Update items, then async compute downstream to a target path. Callback receives "SUCCESS"/"ERROR"/"CANCELLED". |
updateResultCampaignKey(campaignId, path, content, btTitle) | Update raw result (not override). Uses SKIP_OVERRIDDEN_RESET_HIERARCHY. |
updateCampaignApplicationState(campaignId, stateJson) | Set the Customer App's application state |
dropAndCreateStepByTemplate(graph, templatePath, targetPath, defaults, btTitle) | Delete a step and replace it with a clone from a template, pre-filling values |
createPhaseByTemplate(graph, templateName, targetName, classes, btTitle) | Clone a phase with a new name |
createPhasesByTemplateBatch(campaignId, graph, configs, classes, btTitle) | Batch create multiple phases from templates |
renameAndRecomputePhase(campaignId, oldName, newName, btTitle) | Rename a phase and trigger full recomputation |
updateStepClasses(stepId, classes[]) | Update step metadata tags |
graph - Computation Graph Control
Graph computation methods
| Method | What it does |
|---|---|
loadGraph(campaignId) | Load campaign computation graph from DB |
findItemByPath(graph, path) | Find item in loaded graph |
computeCampaign(graph, btTitle) | Compute entire campaign (async) |
computeCampaign(graph, callback, btTitle) | Same with JS callback on completion |
computePhase(graph, phaseName, btTitle) | Compute single phase |
computeStep(graph, type, execType, scope, stepId, btTitle) | Compute step with full control over type/scope |
computeItem(graph, type, execType, scope, path) | Compute single item with full control |
computeCampaignUpToPath(graph, upToPath, btTitle) | Compute from start up to a specific item path |
computeCampaignUpToPath(graph, scope, upToPath, callback, btTitle) | Same with custom scope and callback |
log - Debug Logging
| Method | What it does |
|---|---|
enable() / disable() | Toggle debug logging for this execution |
info(message) / error(message) | Log messages (only if logging enabled via user preference) |
event(item, message) | Publish a UI-visible event for a specific CC item |
Other injected context
| Object | What it provides |
|---|---|
| Parent values | All parent variable results injected by their key name as JS variables |
| Environment functions | Env vars of type "prompt" become callable functions: myPrompt(arg1, arg2) substitutes %s placeholders |
| JS libraries | All js_lib env vars loaded and executed before the script runs |
| item | The current CC item being computed |
Deferred Computation
Automatic Retry for Missing Dependencies
If a JavaScript function references a variable that hasn't been computed yet (ReferenceError), the system doesn't fail - it defers this item and tries again later. This handles implicit dependencies in script code that aren't captured by the declared dependency graph.
Cross-Campaign References
Scripts can access data from other campaigns using campaignOpsApi.externalKey(companyName, campaignName, "Phase/Step/Key"). This enables:
- Pulling reference data from a "master data" campaign
- Comparing results across campaigns
- Building aggregation scripts that read from multiple sources
Async Computation with Callbacks
Scripts can trigger computations and receive a callback when they complete:
campaignOpsApi.updateCampaignKeysAndCompute(
campaignId,
{"Phase/Step/headline": "New headline value"},
"Phase/Step/final_document", // compute up to this path
function(status) {
if (status === "SUCCESS") log.info("Downstream computed!");
if (status === "ERROR") log.error("Computation failed");
},
"Updated headline and recomputed"
);
4. Variable Type Options (CC Item Configuration)
Each CC item type has a hidden configuration object (options) that controls its behavior. These options aren't always visible in the UI but fundamentally change how the variable works.
Prompt Variables (prompt, json_prompt, image_prompt)
| Option | Type | What it controls |
|---|---|---|
aiModelId | Long | Override the campaign's default AI model for this specific variable |
promptKey | String | Environment variable key containing the prompt template |
arguments | String[] | Arguments for %s placeholder substitution in the template |
resultType | Enum | ORIGINAL (AI output), OVERRIDE (user value), PATCHED (AI merge of both) |
postProcessingParams | JSON | Chain of additional prompts applied after the main result (see post-processing below) |
json_prompt adds: jsonSchemaKey - env var key for JSON Schema v4 that constrains AI output.
image_prompt adds: imageUrl - URL of an image for vision model analysis (png, jpg, gif, webp).
Session Prompt (session_prompt)
| Option | What it controls |
|---|---|
sessionName | Conversation topic identifier (default: ${phase.name}-${step.name}). Items with the same session name share conversation history. |
sessionInstruction | System prompt / instructions for the conversation. Sets the AI's behavior and context. |
promptKey | Prompt template for user messages |
arguments | Template arguments |
DALL-E Image Generation (dalle)
| Option | What it controls |
|---|---|
imageGenModel | Only DALL_E_3 currently supported |
imageSize | 1024x1024 (square), 1024x1792 (portrait), 1792x1024 (landscape) |
imagePrompt | Text description of the image to generate |
assetTitle | Name for the generated asset (supports ${variable} placeholders) |
microSiteTargetFolder | Deployment path on microsite |
Asset Generation (asset)
| Option | What it controls |
|---|---|
documentTemplateId | Which document template to render |
assetTitle | Asset filename (supports ${variable} placeholders) |
buildPdfAsset | If true, also generates a PDF from the HTML output |
pdfPageSize | Page dimensions: "210x297 mm" (A4 default), "8.27x11.69 in", or ${variable} |
initializeFullContext | If true, loads ALL campaign data into template context (expensive). Default: only dependencies. |
overwriteIfExists | Whether to overwrite existing asset file (default: true) |
microSiteTargetFolder | Deployment path (supports ${company}, ${campaign}, ${phase}, ${step} placeholders) |
Web Scraper (web_scraper)
| Option | What it controls |
|---|---|
webUrl | Target URL (supports ${variable} placeholders) |
requestTimeout | Timeout in ms (default: 10000) |
scrapHtml | If true, parses HTML and extracts content via XPath |
xpathSelector | XPath expression for targeted content extraction |
service | VESCRAPER (internal), SCRAPINGBEE (external), SCRAPINGBEE_RENDER_JS (with JavaScript rendering) |
waitingDuration | Milliseconds to wait for JS rendering (ScrapingBee only, 0-10000) |
Function (function)
| Option | What it controls |
|---|---|
scriptContent | JavaScript code to execute |
cacheResult | If true, caches the result and skips re-execution on subsequent runs |
File Content (file_content)
| Option | What it controls |
|---|---|
fileUrl | URL to fetch file from (supports ${variable} placeholders). Formats: txt, csv, pdf, doc, docx, xls, xlsx, ppt, pptx. |
5. Prompt Post-Processing & AI Patching
Post-Processing Chain
After a prompt variable produces its initial result, post-processing can apply a chain of additional LLM prompts to refine it:
flowchart TD
Initial["Initial LLM result"] --> PP1["Post-processing prompt 1
(e.g., improve grammar)"]
PP1 --> PP2["Post-processing prompt 2
(e.g., summarize)"]
PP2 --> Final["Final result stored"]
Initial -.->|"Available as ${0}"| PP1
PP1 -.->|"Available as ${1}"| PP2
Each post-processing step can reference:
| Source type | How it works |
|---|---|
cc | Use another CC item's value as input |
env | Use an environment variable as input |
text | Use literal text as the prompt |
Previous results in the chain are available as ${0}, ${1}, etc. This enables multi-stage refinement pipelines.
AI Patching (Semantic Merge)
When a prompt variable has resultType = PATCHED, the system performs a two-step AI merge:
- Compute Patch - AI analyzes the semantic difference between the original result and the user's override:
Prompt: "What changed between: [original] and: [override]?" Result stored in: item.patch
- Apply Patch - AI merges the original with the semantic patch to create a final version:
Prompt: "Starting with: [original], apply this change: [patch]" Result stored in: item.patched
The patching prompts are loaded from environment variables:
{key}-ComputePatch(key-specific) orComputePatch(global fallback){key}-ComputePatchedContent(key-specific) orComputePatchedContent(global fallback)
When to use patching vs. override
Override: Use when you want to completely replace the AI output with your own text. The original is ignored.
Patching: Use when you want the AI to intelligently incorporate your changes into its original output. Good for tweaking tone, adding specific details, or correcting facts while keeping the AI's structure.