Webhook Payloads
When an alert is delivered to a webhook channel, FORJ sends the full alert envelope — a JSON object containing everything about the moment the alert fired: what triggered it, the strategy's state, the event payload, indicator values, and trade parameters.
You can control the shape of the webhook payload using three transform modes: passthrough (send everything), mapping (pick specific fields), or template (build a custom JSON structure with expressions).
Alert envelope
Every alert produces an envelope with these top-level fields:
| Field | Type | Description |
|---|---|---|
alertDefinitionId | string | Unique ID of the alert definition that fired |
alertDefinitionName | string | Name you gave the alert definition |
alertDefinitionDescription | string? | Optional description |
alertKind | string | GUARD, TRANSITION, ACTION, or EVENT |
severity | string | INFO, WARNING, or CRITICAL |
workspaceId | string | Workspace ID |
stateMachineConfigId | string | Strategy ID |
triggerTs | number | Timestamp when the alert fired (epoch seconds) |
contextSymbol | string? | Symbol from the strategy context |
streamSymbol | string? | Symbol from the data stream that produced the event |
cause | object | What triggered the alert — shape varies by alertKind (see below) |
event | object | The incoming event that was being processed when the alert fired |
machineEvent | object | Strategy execution metadata for this event |
Cause object
The cause object shape depends on the alert kind:
Action cause (alertKind: ACTION)
| Field | Type | Description |
|---|---|---|
cause.kind | string | ACTION |
cause.actionId | string | ID of the action that executed |
cause.actionName | string | Name of the action |
cause.actionType | string | Action type (e.g., predefined) |
cause.fromState | string | State the strategy was in before the transition |
cause.toState | string | State the strategy transitioned to |
cause.predefinedActionKind | string? | EXECUTE_TRADE when the action contains an Execute Trade step |
cause.predefinedConfig | object? | Resolved trade parameters (see below) |
When an Execute Trade action fires, cause.predefinedConfig contains:
| Field | Type | Description |
|---|---|---|
cause.predefinedConfig.sl_price | number? | Resolved stop loss price |
cause.predefinedConfig.tp_price | number? | Resolved take profit price |
cause.predefinedConfig.symbol | string? | Trading symbol |
cause.predefinedConfig.timestamp | number? | Execution timestamp (ms) |
cause.predefinedConfig.entryPrice | number? | Entry price |
cause.predefinedConfig.direction | string? | long or short |
cause.predefinedConfig.orderType | string? | market, limit, stop, or stop_limit |
cause.predefinedConfig.tradePlanId | string? | Trade plan ID |
cause.predefinedConfig.tradePlanName | string? | Trade plan name |
cause.predefinedConfig.lifecycleId | string? | Links entry and exit events |
Transition cause (alertKind: TRANSITION)
| Field | Type | Description |
|---|---|---|
cause.kind | string | TRANSITION |
cause.fromState | string | Previous state |
cause.toState | string | New state |
Guard cause (alertKind: GUARD)
| Field | Type | Description |
|---|---|---|
cause.kind | string | GUARD |
cause.guardId | string | ID of the guard that was evaluated |
cause.guardName | string | Name of the guard |
cause.result | boolean | Whether the guard passed or failed |
Event cause (alertKind: EVENT)
| Field | Type | Description |
|---|---|---|
cause.kind | string | EVENT |
cause.eventName | string | Name of the system event (e.g., ACK_TRADE_EXECUTED) |
cause.triggerKind | string | Event category |
Event payload
The event object contains the incoming market data:
| Field | Type | Description |
|---|---|---|
event.type | string | Event type name |
event.payload.open | number? | Candle open price |
event.payload.high | number? | Candle high price |
event.payload.low | number? | Candle low price |
event.payload.close | number? | Candle close price |
event.payload.volume | number? | Candle volume |
event.payload.timestamp | number? | Event timestamp |
event.payload.indicators.triggerStreamId | string? | Physical stream ID that triggered the event |
event.payload.indicators.byStream | object? | Indicator snapshots keyed by physical stream ID |
event.payload.indicators.byStream.<streamId>.byIndicator | object? | Indicator values keyed by workspace indicator config ID |
The live runtime indicator contract is stream-qualified. A typical payload shape looks like this:
{
"triggerStreamId": "stream_eurusd_1m",
"byStream": {
"stream_eurusd_1m": {
"ts": 1703548800,
"byIndicator": {
"wic_rsi_14": {
"name": "RSI_14_EMA_close",
"value": 65.4,
"params": {
"length": 14,
"smoothing": "EMA",
"priceSource": "close"
},
"outputs": ["value"]
}
}
}
}
}
Machine event metadata
| Field | Type | Description |
|---|---|---|
machineEvent.triggerKind | string | How the event was triggered |
machineEvent.triggerName | string? | Name of the trigger |
machineEvent.hasRealChange | boolean | Whether the strategy's state actually changed |
machineEvent.inferredCandleTsSec | number? | Inferred candle timestamp (epoch seconds) |
Transform modes
Passthrough
The default mode. Sends the entire alert envelope as the webhook payload with no transformation. Useful when your receiving system can parse the full JSON and extract what it needs.
Mapping
Select specific fields from the envelope, optionally rename them, and apply a filter. The webhook payload contains only the fields you specify.
Each field mapping has:
| Setting | Description |
|---|---|
| Source | Dot-path to a field in the alert envelope (e.g., cause.toState) |
| Target | Name the field should have in the output payload |
| Filter | Optional filter to apply to the value |
| Default | Fallback value if the source field is missing |
Example mapping:
| Source | Target | Filter | Default |
|---|---|---|---|
alertDefinitionName | alertName | ||
cause.toState | newState | upper | |
event.payload.close | price | number | |
cause.predefinedConfig.sl_price | stopLoss | 0 |
Produces:
{
"alertName": "My Trade Alert",
"newState": "IN_POSITION",
"price": 1.0842,
"stopLoss": 1.0822
}
Template
Build a custom JSON structure using {{expression}} placeholders. Any string value in the template is interpolated; non-string values (numbers, booleans) pass through unchanged.
Example template:
{
"content": "Trade signal: {{cause.predefinedConfig.direction | upper}} {{cause.predefinedConfig.symbol}}",
"embeds": [
{
"title": "{{alertDefinitionName}}",
"fields": [
{ "name": "Entry", "value": "{{cause.predefinedConfig.entryPrice}}" },
{ "name": "SL", "value": "{{cause.predefinedConfig.sl_price}}" },
{ "name": "TP", "value": "{{cause.predefinedConfig.tp_price}}" },
{ "name": "State", "value": "{{cause.fromState}} → {{cause.toState}}" }
]
}
]
}
This format works directly as a Discord webhook payload, for example.
Expression syntax
Expressions use {{path}} to reference a value from the alert envelope. The path is a dot-separated key sequence that walks into the envelope object.
Type preservation
When a template value is a single expression with no surrounding text, the resolved value keeps its original type:
| Template value | Result | Type |
|---|---|---|
"{{event.payload.close}}" | 1.0842 | number |
"{{machineEvent.hasRealChange}}" | true | boolean |
"{{cause}}" | { ... } | object |
When a template value mixes text with expressions, the result is always a string:
| Template value | Result | Type |
|---|---|---|
"Price: {{event.payload.close}}" | "Price: 1.0842" | string |
"{{cause.fromState}} → {{cause.toState}}" | "Scanning → In Position" | string |
Missing values
If a path points to a field that doesn't exist:
- Single expression: resolves to
undefined - Mixed text: the placeholder is replaced with an empty string
Use the default filter to provide a fallback: {{cause.guardName | default:unknown}}
Array access
Use numeric indices in the dot-path to access array elements. For nested indicator values, use the stream-qualified path such as {{event.payload.indicators.byStream.stream_eurusd_1m.byIndicator.wic_rsi_14.value}}. For arrays, use {{some.array.0}} for the first element.
Filters
Filters transform a resolved value before it's included in the output. Chain them with the pipe character (|):
{{path | filter1 | filter2:arg}}
Filters are applied left-to-right. Each filter receives the output of the previous one.
| Filter | What it does | Example input | Example output |
|---|---|---|---|
upper | Uppercases a string | "in_position" | "IN_POSITION" |
lower | Lowercases a string | "CRITICAL" | "critical" |
number | Converts to a number (strings are parsed, non-numeric values become 0) | "3.14" | 3.14 |
boolean | Converts to boolean | 0 | false |
string | Converts to string | 42 | "42" |
json | Serializes to a JSON string | { a: 1 } | "{\"a\":1}" |
date | Converts a timestamp to an ISO 8601 date string | 1704067200 | "2024-01-01T00:00:00.000Z" |
default:val | Uses val if the value is null or undefined | undefined | "val" |
truncate:N | Truncates a string to N characters (adds ...) | "Hello World" (N=5) | "Hello..." |
Filter details
upper / lower — Only affect strings. Non-string values pass through unchanged.
number — Strings are parsed with parseFloat. If the result is NaN, returns 0. Non-string, non-number values return 0.
date — Accepts epoch seconds or milliseconds. Timestamps below 1,000,000,000,000 are treated as seconds and multiplied by 1000; above that threshold they're treated as milliseconds. Produces an ISO 8601 string like 2024-01-01T00:00:00.000Z.
default:val — Only triggers on null or undefined. Values like 0, "", and false are kept as-is. The fallback value is always a string.
truncate:N — Defaults to 100 characters if N is omitted. Only affects strings. The truncated output is the first N characters followed by ....
Filter chaining example
{{alertDefinitionName | upper | truncate:20}}
- Resolve
alertDefinitionName→"My Long Strategy Alert Name" - Apply
upper→"MY LONG STRATEGY ALERT NAME" - Apply
truncate:20→"MY LONG STRATEGY ALE..."