Manifest Types
A NodeExecutorManifest comes in two forms: Empty (a minimal
placeholder) and From (a full field + suspension declaration). Knowing
which to use and when is the first decision when adding policy support to a node.
Empty Manifest
NodeExecutorManifest.Empty(ProcessElementTypeCode)
An empty manifest tells the platform: "This node exists and is registered, but has no field policy declarations. Apply defaults for all cross-cutting concerns."
When to use Empty
- The node has no fields requiring expression evaluation, data routing, HIL, or masking
- The node is a simple action — no human interaction, no sensitive data
- You are writing a new node and the full manifest is not yet designed (use Empty as a placeholder)
What the platform does with Empty
| Concern | Behaviour with Empty |
|---|---|
| Expression evaluation | No field-level evaluation occurs |
| Data flow | Input/output mapping skipped; node receives raw data |
| HIL rendering | No fields sent to human inbox |
| Security masking | No masking applied |
| Suspension | No timers registered |
// StopWorkflow node — no fields, just halts execution
protected override NodeExecutorManifest? GetNodeExecutorManifest()
=> NodeExecutorManifest.Empty(ProcessElementTypeCode);
Full Manifest (From)
NodeExecutorManifest.From(
ProcessElementTypeCode,
fields: new[] { /* NodeFieldDescriptor[] */ },
suspensionPolicy: null // or: new SuspensionPolicy { ... }
)
A full manifest declares every field the node cares about and, optionally, a
SuspensionPolicy if the node pauses and waits for human input.
When to use From
- The node has fields requiring evaluation, routing, HIL display, or security handling
- The node suspends and waits for human input (approval, form, chat)
- You need to control which fields a human reviewer sees and what they can do
Full example
protected override NodeExecutorManifest? GetNodeExecutorManifest()
=> NodeExecutorManifest.From(
nodeTypeName: ProcessElementTypeCode,
fields: new[]
{
new NodeFieldDescriptor
{
FieldId = "to",
Description = "Recipient 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()
}
// ... more descriptors
},
suspensionPolicy: new SuspensionPolicy
{
TimeoutSeconds = 86400,
TimeoutPortKey = "expired",
TimeoutBehavior = TimeoutBehavior.AbsoluteDeadline
}
// or: suspensionPolicy: null
);
Extension JSON (Database Overrides)
Extension JSON does not use "empty" vs "full" — it just lists the fields to add or override. The resolver merges JSON fields with the code manifest; only the properties present in JSON are overridden. Everything else keeps its code-defined values.
{
"nodeTypeName": "email-smtp",
"suspensionPolicy": null,
"fields": [
{
"id": "body",
"hilPolicy": {
"sendToHil": true,
"inputMode": "RequiredFromHuman",
"label": "Email Body",
"description": "Compliance requires you to confirm every email body."
}
}
]
}
Registration Flow
DI container loads executor assembly
AddSmtpNodeExecutor() or AddExecutorPluginsFromAssembly() registers the executor.
GetNodeExecutorManifest() invoked
Called once per executor type. Result stored in NodeFieldManifestRegistry keyed by node type name.
Resolver merges at execution time
NodeFieldManifestResolver.GetManifest(nodeTypeName, extensionJson) — code manifest + DB JSON → final resolved manifest.
Platform subsystems read the merged result
Expression evaluator, data flow mapper, HIL renderer, security layer, and suspension orchestrator all read from the final manifest.
GetNodeExecutorManifest() returns null the platform treats it identically to Empty. The base class default returns null. Always return Empty(ProcessElementTypeCode) explicitly to make intent clear.