Overview
What Node Policies are, why they exist, the two-layer architecture, and how the platform reads them at runtime to handle expression evaluation, data routing, HIL rendering, security masking, and suspension.
The Problem Without Node Policies
Without a contract, every node is a black box. The platform cannot:
- Route data from upstream nodes to the right fields without per-node custom code
- Know which fields a human reviewer should see or be able to edit
- Apply consistent credential masking rules across all 60+ node types
- Register suspension timers when a node waits for human input
- Evaluate template expressions at the correct lifecycle stage
Node Policies solve this by giving every node a machine-readable manifest that the platform reads to handle all of these concerns automatically.
The Five Policy Types
| Policy | Controls |
|---|---|
| ExpressionPolicy | When and how a field's value is computed (evaluation stage + evaluator kind) |
| DataFlowPolicy | Which fields accept upstream data, emit to downstream, and persist to workflow memory |
| HilPolicy | What humans see in their task inbox, how it is displayed, and what they can change |
| SecurityPolicy | Field masking in logs and output, and elevated-access gates |
| SuspensionPolicy | Timeout, SLA thresholds, reminders, and escalation for nodes that wait |
The Two Layers
Layer 1 — Code Manifest (Compile-time)
Each node executor defines its manifest in a .Config.cs partial by overriding
GetNodeExecutorManifest():
// SmtpNodeExecutor.Config.cs
protected override NodeExecutorManifest? GetNodeExecutorManifest()
=> NodeExecutorManifest.From(
ProcessElementTypeCode,
new[]
{
new NodeFieldDescriptor
{
FieldId = "to",
Description = "Recipient email address",
ExpressionPolicy = new ExpressionPolicy
{
EvaluationStage = EvaluationStage.AtInputReady,
EvaluatorKind = EvaluatorKind.Template
},
DataFlowPolicy = new DataFlowPolicy
{
AcceptsUpstreamInput = true,
ExcludeFromOutputMapping = true
},
HilPolicy = new HilPolicy
{
SendToHil = true,
DisplayMode = HilDisplayMode.ReadableContext,
InputMode = HilInputMode.EditableOptional,
Label = "To"
},
SecurityPolicy = new SecurityPolicy()
}
}
);
Layer 2 — Extension JSON (Runtime Override)
Administrators store additional field descriptors or override existing ones in the database
(ProcessNodePolicies table). This JSON is merged with the code manifest at
runtime by NodeFieldManifestResolver, allowing customisation without
redeployment — for example adjusting HIL labels or enabling masking for a specific tenant.
{
"nodeTypeName": "email-smtp",
"fields": [
{
"id": "body",
"hilPolicy": {
"sendToHil": true,
"inputMode": "RequiredFromHuman",
"label": "Email Body",
"description": "You must confirm this before the email sends."
}
}
]
}
Runtime Execution Flow
Manifest registered at startup
GetNodeExecutorManifest() is called once per node type. The result is stored in NodeFieldManifestRegistry keyed by ProcessElementTypeCode.
Resolver merges code + DB overrides
NodeFieldManifestResolver.GetManifest(nodeTypeName, extensionJson) produces the final merged manifest for the execution.
Expression evaluation — AtConfigLoad stage
Fields with EvaluationStage.AtConfigLoad are resolved using the configured evaluator (Template, Literal, etc.).
DataFlow — input mapping
Fields with AcceptsUpstreamInput: true are populated from the upstream node's output data bag. Fields with ExcludeFromInputMapping: true are skipped.
Node executes
The executor runs its logic. Output fields are populated by the executor and held in the result.
DataFlow — output mapping
Fields with EmitsToDownstream: true are written to the downstream data bag. Fields with PersistsToMemory: true are written to workflow memory.
Security masking
Fields with MaskInLogs: true or MaskInOutput: true are redacted before logs are written and before the output bag leaves the node.
Suspension timers registered (if node suspends)
SuspensionPolicyOrchestrator reads the SuspensionPolicy and registers timeout and reminder timers via ISuspensionTimerService.
HIL fields sent to inbox (if in review mode)
Fields with SendToHil: true are formatted and routed to the actor's inbox. DisplayMode and InputMode control rendering.
Key Types
| Type | Role | Project |
|---|---|---|
NodeExecutorManifest | Container for all field descriptors + suspension policy for one node type | BizFirst.Ai.ProcessNodePolicies.Domain |
NodeFieldDescriptor | One field and its four per-field policies | Same |
ExpressionPolicy | Evaluation stage + evaluator kind | Same |
DataFlowPolicy | Data routing rules | Same |
HilPolicy | Human inbox rendering rules | Same |
SecurityPolicy | Masking and access rules | Same |
SuspensionPolicy | Timeout and reminder configuration | Same |
NodeFieldManifestRegistry | Thread-safe ConcurrentDictionary of code manifests | BizFirst.Ai.ProcessNodePolicies.Service |
NodeFieldManifestResolver | Merges code manifest with DB extension JSON | Same |
SuspensionPolicyOrchestrator | Reads SuspensionPolicy, registers all timers at suspension time | BizFirst.Ai.ProcessEngine.Service |
NodeFieldManifest is aliased to NodeExecutorManifest in the GlobalUsings files:global using NodeFieldManifest = BizFirst.Ai.ProcessNodePolicies.Domain.Models.NodeExecutorManifest;