Message Templating
How the Jint expression engine evaluates message fields at send time — interpolating workflow data into text, HTML, and Block Kit payloads using $output, $json, and $context variables.
Expression Context
All string fields in messaging node config are evaluated as expressions before sending. The expression context is identical to the rest of the Flow Studio expression engine:
| Variable | Type | Description |
|---|---|---|
$output.{nodeId} | object | Output of a completed upstream node |
$json | object | Trigger payload / current loop item |
$context | object | Execution metadata (executionId, tenantId, actorId, appUrl, workDeskUrl) |
$env | object | Environment variables exposed to the workflow |
$now | Date | Current UTC timestamp as JavaScript Date object |
String Interpolation
For simple value injection, use the $variable.path prefix in a string field. The engine detects and substitutes single expressions:
{
"text": "Invoice $output.createInvoice.invoiceNumber is ready for review.",
"subject": "Action Required — $output.createInvoice.invoiceNumber ($output.fetchVendor.name)",
"body": "Hi $output.fetchEmployee.firstName, your request for $output.createExpense.amount $output.createExpense.currency has been submitted."
}
Full Expression Mode
For computed values, concatenation, or conditional content, use a full JavaScript expression (the entire string is the expression):
// Conditional subject line
"subject": "$output.reviewDecision.approved ? 'Approved: ' + $output.createInvoice.invoiceNumber : 'Rejected: ' + $output.createInvoice.invoiceNumber"
// Formatted currency amount
"text": "'Total approved: ' + new Intl.NumberFormat('en-US', { style: 'currency', currency: $output.createInvoice.currency }).format($output.createInvoice.total)"
// Date formatting
"text": "'Submitted on ' + new Date($output.createExpense.submittedAt).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })"
HTML Body Templating
The bodyHtml field for email nodes supports {{expression}} interpolation inside a larger HTML string. This allows building rich HTML emails with inline data:
<!-- bodyHtml value -->
<h2>Hello {{$output.fetchEmployee.firstName}},</h2>
<p>Invoice <strong>{{$output.createInvoice.invoiceNumber}}</strong>
for <strong>{{$output.fetchVendor.name}}</strong>
totalling <strong>${{$output.createInvoice.total.toFixed(2)}}</strong>
requires your approval.</p>
<table>
{{$output.createInvoice.lineItems.map(item =>
'<tr><td>' + item.description + '</td><td>$' + item.amount.toFixed(2) + '</td></tr>'
).join('')}}
</table>
<a href="{{$context.workDeskUrl}}/tasks/{{$context.executionId}}">Review Now</a>
Block Kit Expression Building
The Slack blocks field accepts a JavaScript expression that returns a Block Kit array. Use this to build dynamic blocks from workflow data:
// blocks field — full expression returning an array
`[
{
type: 'header',
text: { type: 'plain_text', text: 'Expense Report: ' + $output.createExpense.reportName }
},
{
type: 'section',
fields: $output.createExpense.items.slice(0, 10).map(item => ({
type: 'mrkdwn',
text: '*' + item.category + '*\n$' + item.amount.toFixed(2)
}))
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: $output.reviewDecision.approved
? ':white_check_mark: *Approved* by ' + $output.reviewDecision.approverName
: ':x: *Pending approval*'
}
}
]`
Null-Safe Access
Use null-coalescing and optional chaining to handle missing data gracefully:
// Null-safe with fallback
"text": "$output.fetchEmployee.displayName ?? $output.fetchEmployee.email ?? 'Unknown user'"
// Optional chaining
"text": "$output.createInvoice.vendor?.name ?? 'No vendor'"
// Conditional segment
"body": "'Hello ' + ($output.fetchEmployee.firstName ?? 'there') + ', your request has been received.'"