HIL Labels
When a workflow suspends for human review, it builds a HIL (Human-in-the-Loop) payload that describes every field the human should see, interact with, or approve. The labels and descriptions in that payload are not static strings — they are expression templates evaluated at suspension time, making them fully dynamic.
The HIL Payload Build Process
IHilLabelResolver
IHilLabelResolver is the interface responsible for evaluating the
Label and Description strings declared in a field's
HilPolicy. It receives the raw label string and the full
expression context at the point of suspension, then returns the resolved string.
Because labels are resolved at suspension time — after all upstream nodes have run and all Tier 1 and Tier 2 expressions have been evaluated — they have access to the full set of runtime directives:
| Directive | In label expressions | Example |
|---|---|---|
@{input:key} | Yes | "Employee: @{input:firstName} @{input:lastName}" |
@{output:nodeKey.field} | Yes | "Salary calculated by: @{output:calcNode.method}" |
@{var:key} | Yes | "Threshold: @{var:approvalLimit}" |
@{env:KEY} | Yes | "Environment: @{env:DEPLOY_ENV}" |
@{context:field} | Yes | "Process ID: @{context:processId}" |
Label expressions are always evaluated in template mode (see Expression Syntax). The result is always a string. Strict mode is not supported for labels.
Declaring HIL Labels in a Field Manifest
public class ApprovalNodeSettings : BaseNodeExecutorSettings, INodeFieldManifestSource
{
public string EmployeeId { get; set; }
public decimal SalaryAmount { get; set; }
public string Department { get; set; }
public NodeFieldManifest GetFieldManifest() => new NodeFieldManifest()
.Add(nameof(EmployeeId), f => f
.Expression(EvaluationStage.AtInputReady, EvaluatorKind.Template)
.DataFlow(DataFlowRole.Input)
.Hil(h => h
.SendToHil(
displayMode: DisplayMode.ReadableContext,
inputMode: InputMode.Locked,
label: "Employee",
description: "@{output:fetchEmployee.fullName} — @{output:fetchEmployee.jobTitle}")))
.Add(nameof(SalaryAmount), f => f
.Expression(EvaluationStage.AtInputReady, EvaluatorKind.Template)
.DataFlow(DataFlowRole.Input)
.Hil(h => h
.SendToHil(
displayMode: DisplayMode.ReadableContext,
inputMode: InputMode.PrefilledEditable,
label: "Proposed Salary",
description: "Current salary: @{output:fetchEmployee.currentSalary}")))
.Add(nameof(Department), f => f
.Expression(EvaluationStage.AtInputReady, EvaluatorKind.Template)
.DataFlow(DataFlowRole.Input)
.Hil(h => h
.SendToHil(
displayMode: DisplayMode.ReadableContext,
inputMode: InputMode.Locked,
label: "Department",
description: "")));
}
HilFieldDescriptor — The Output Object
After label resolution, each field with SendToHil = true becomes a
HilFieldDescriptor entry in the payload. This is what the Studio UI
consumes to render the approval form.
| Property | Source | Description |
|---|---|---|
FieldKey | Field name from manifest | Identifies which config field this entry describes |
Label | HilPolicy.Label after expression resolution | Human-readable label shown in the form |
Description | HilPolicy.Description after expression resolution | Longer contextual description shown under the label |
CurrentValue | Resolved field value at time of suspension | The value the node had when it suspended |
DisplayMode | HilPolicy.DisplayMode | How the value is rendered (visible / masked / concealed) |
InputMode | HilPolicy.InputMode | Whether the human can edit the value |
FieldType | Inferred from value type | Used by the UI to render the appropriate input control |
What Happens on Resume
When the human submits the HIL form, the submitted values are received by the
ContinueAsync handler. For each field where InputMode
is not Locked, the submitted value replaces the current field value
in the suspended execution's config bag.
When the thread resumes execution, the node's config fields reflect the human's
input. If the human changed SalaryAmount from 80,000 to 85,000,
the executor will see 85,000 as its SalaryAmount setting when it
runs after resumption.
PreHilData and the Audit Trail
Before the HIL payload is built, the current state of all fields is snapshotted
into PreHilData. This snapshot is persisted alongside
SuspendedExecutionData and is never modified by the human's input.
After resumption, both PreHilData (the before state) and
Hil (the human's submitted values) are available for comparison
in audit trails, compliance checks, and post-hoc analysis. The executor can
access both bags via the element context if it needs to act on the delta.
"Approve salary increase for @{output:fetchEmployee.fullName}"
are not validated until the node actually suspends at runtime. A typo in an
element key will produce a label with unresolved placeholders, not a deployment-time
error. Always test HIL nodes end-to-end to verify label rendering.