Message Push
Message push flows turn an external HTTP request into a Soln notification. The external system posts a JSON payload to the flow webhook, the flow renders that payload into a message, and the message is delivered to an app inbox or a group chat.
A message push flow belongs to an OpenLink system and differs from inbound and outbound flows:
- It has no form binding and never creates, updates, or deletes local form records
- Its canvas contains exactly one start node, at least one message node, and one end node. Source, transform, target, branch, and query nodes are not allowed
- It is always triggered by
webhook; poll, push, and service call triggers are not available - An agent execution node may run before the message node. When it does, the agent output becomes the message body
Message Node Configuration
Select the message node in the design canvas to configure how the notification is delivered.
| Field | Description |
|---|---|
| Delivery type | Inbox message sends an app message; Group chat message posts into a group chat |
| App channel | Inbox only. A non-Luna app channel that receives the message |
| Message template | Inbox only. An active inbox template. Because a message push flow has no form binding, the template cannot be a form record template |
| Recipients | Inbox only. Fixed members, a form member field, or members resolved from the request body |
| Sender agent | Group chat only. Provides the sending identity inside the group chat. The message node does not execute the agent |
| Target group chat | Group chat only. The group chat must already contain the sender agent |
| Execution condition | Always, On success, or On failure. It applies when no agent execution node feeds the message |
Message Body
| Situation | Message body |
|---|---|
| An agent execution node ran before the message | The agent output text, sent as-is |
| No agent node, and the execution failed | The failure reason |
No agent node, and the payload has a top-level markdown string | That Markdown, sent as-is |
| No agent node and no failure | Markdown rendered from the webhook payload or the upstream result |
Markdown rendering turns objects into - **field**: value lines, renders arrays of objects inline (- **code**: A · **qty**: 2), skips empty values, and truncates oversized bodies. The sender agent in a group chat is only a carrier: without an agent execution node, the flow does not call AI to produce the content.
How To Request
| Item | Value |
|---|---|
| Method | POST |
| URL | <your-runtime-origin>/api/open-link/message-push/<flowCode> |
| Content type | application/json; other content types return OPEN_LINK_CONTENT_TYPE_INVALID |
| Headers | X-Idempotency-Key is optional; add the inbound authentication headers configured for that system, see Inbound Auth |
| Body | One JSON object, with the parameters listed below; a body that is not a JSON object (an array or a single value) fails with OPEN_LINK_EXECUTION_PAYLOAD_INVALID |
<your-runtime-origin> is the address of the workspace runtime, the same address you use to open Soln in the browser, such as https://runtime.example.com. <flowCode> is the flow code shown on the flow record: a generated string such as open_link_flow_889b5ea7abae, not the flow definition UUID. The flow's start node in the design canvas shows the complete URL for that flow, so copy it from there instead of building it by hand:
https://runtime.example.com/api/open-link/message-push/open_link_flow_889b5ea7abae
Message push has its own endpoint: the inbound webhook URL is reserved for inbound flows and rejects message push flows.
Request Parameters
Every request body parameter is optional. Pick the ones the message node needs; one request can carry several of them.
| Parameter | Type | Required | Description |
|---|---|---|---|
markdown | string | No | Custom group chat body, sent as Markdown without changes |
memberIds | string[] | No | Recipients provided by the request body: member UUIDs or external member identifiers, for example ["1001"], see below |
message | object | No | Inbox message template variables, keyed by the template variable name |
| Any other field | Any | No | Input for an agent execution node, or for node input mappings that read input.* and record.data.* |
Request Examples
Group chat message with a Markdown body you wrote (the message node must use Group chat message):
curl --request POST \
'https://runtime.example.com/api/open-link/message-push/notification-push' \
--header 'Content-Type: application/json' \
--header 'X-OpenLink-Token: ${OPENLINK_TOKEN}' \
--header 'X-Idempotency-Key: notification-001' \
--data '{
"markdown": "> Life is a long stretch of dull hours, with the rose of joy only rarely scattered among them;\n> and whoever would be the first to take that rose of joy is the first to be stung by its thorns.\n\n— Thomas Moore"
}'
Group chat message whose body is rendered from the payload, with recipients passed in memberIds:
curl --request POST \
'https://runtime.example.com/api/open-link/message-push/order-alert-push' \
--header 'Content-Type: application/json' \
--header 'X-OpenLink-Token: ${OPENLINK_TOKEN}' \
--data '{
"memberIds": ["1001"],
"stage": "review",
"owner": "A. Lee",
"tags": ["draft", "review"]
}'
Inbox message with template variables in message (the example template declares quote and signature):
curl --request POST \
'https://runtime.example.com/api/open-link/message-push/quote-push' \
--header 'Content-Type: application/json' \
--header 'X-OpenLink-Token: ${OPENLINK_TOKEN}' \
--data '{
"message": {
"quote": "Freedom consists less in doing what one wants than in not doing what one does not want.",
"signature": "------ Jean-Jacques Rousseau <Reveries of a Solitary Walker>"
}
}'
Send X-OpenLink-Token only when the OpenLink uses shared-token inbound authentication; use the configured header name. For signature authentication, send the configured app ID, timestamp, and HMAC signature headers instead. The signature must cover this request's exact raw JSON body.
Request Notes
Group chat body. The body is chosen in this order: agent execution output, then the execution failure reason, then a markdown field in the payload, then Markdown generated from the payload. Without a markdown field the payload is rendered: every field becomes one - **field**: value line, a nested object becomes an indented sub-list, an array of objects is inlined as - **date**: 2026-09-15 · **hours**: 2, empty or null values are skipped, and a body longer than 3800 characters is truncated. For example:
{ "stage": "review", "owner": "A. Lee", "tags": ["draft", "review"] }
- **stage**: review
- **owner**: A. Lee
- **tags**
- draft
- review
The body must stay within the chat message limit of 4000 characters; a longer body fails with WORKFLOW_GROUP_CHAT_BODY_TOO_LONG. When an agent execution node runs before the message node, its output is always the group chat body, even when the request carries a markdown field; the request then carries the fields the agent analyzes.
Inbox message template. Template variables are read on the message node through the request body mapping type, resolved as input.message.<variable key>, and the mapping key must be exactly the template variable key. The example template body above is written as {{input.quote}} and {{input.signature}}, and the delivered message is ordinary text, not a field list:
Freedom consists less in doing what one wants than in not doing what one does not want.
------ Jean-Jacques Rousseau <Reveries of a Solitary Walker>
If the request is missing a mapped field, the execution fails and the execution log records the missing path, for example RUNTIME_CONTEXT_PATH_NOT_FOUND:input.message.quote.
Recipients from the request body. On the message node open Configure recipients, switch to the From request body scope and tick Member group. The members resolved from the request body then work exactly like fixed recipients: whoever the request resolves to receives the message. Every value in memberIds is first resolved through the subject mappings configured for this OpenLink system; for example, when 1001 is mapped to 00000000-0000-4000-8000-000000000010, that member receives the message. Values that have no mapping and are not member UUIDs are skipped. Recipients are never inferred from other fields, and when nothing resolves the execution fails with MESSAGE_APP_CHANNEL_TARGETS_UNRESOLVED in the log.
Before the request is accepted: the OpenLink system must be active, the flow must be published, the caller must pass the inbound authentication and whitelist rules configured for that system, and requests count against the inbound rate and concurrency protection.
Response
The endpoint returns 202 Accepted once the execution is queued. The message is delivered asynchronously.
{
"accepted": true,
"duplicate": false,
"logId": "00000000-0000-4000-8000-000000000001",
"traceId": "00000000-0000-4000-8000-000000000002",
"status": "queued",
"definitionId": "00000000-0000-4000-8000-000000000003",
"flowCode": "order-alert-push",
"workflowId": "open-link:...",
"workflowStartDeferred": false,
"callbackRequested": false,
"statusQueryUrl": null
}
| Field | Description |
|---|---|
accepted | The request was accepted for execution |
duplicate | true when the same idempotency key was already accepted; the existing logId and traceId are returned |
logId / traceId | Execution identifiers used to find the run in the execution logs |
status | Initial execution status, queued |
definitionId / flowCode | Flow definition that accepted the request |
callbackRequested / statusQueryUrl | Always false and null for message push: this endpoint creates no asynchronous delivery callback |
Open the flow execution logs with the returned logId to confirm the message was delivered and to read the failure reason when a run failed. Errors raised while the flow runs, such as an unresolved recipient, are recorded in the execution log rather than returned by the HTTP response.
Common Errors
| Code | Meaning |
|---|---|
OPEN_LINK_FLOW_DEFINITION_NOT_FOUND | No flow matches the flow code |
OPEN_LINK_FLOW_DEFINITION_NOT_PUBLISHED | The flow exists but has not been published |
OPEN_LINK_SYSTEM_NOT_ACTIVE | The OpenLink system is not active |
OPEN_LINK_FLOW_EXECUTION_TYPE_INVALID | The flow is not a message push flow; use the inbound webhook or query endpoint instead |
OPEN_LINK_CONTENT_TYPE_INVALID | The request content type is not application/json |
OPEN_LINK_JSON_VALUE_INVALID | The request body is not valid JSON |
OPEN_LINK_PAYLOAD_TOO_LARGE | The request body exceeds the size limit |
OPEN_LINK_WEBHOOK_AUTH_INVALID / OPEN_LINK_WEBHOOK_TIMESTAMP_EXPIRED | Inbound authentication rejected the request |
OPEN_LINK_WEBHOOK_SOURCE_DENIED | Whitelist rules rejected the caller |
OPEN_LINK_INBOUND_RATE_LIMIT_EXCEEDED / OPEN_LINK_INBOUND_CONCURRENCY_LIMIT_EXCEEDED | Inbound rate or concurrency protection rejected the request |
OPEN_LINK_IDEMPOTENCY_KEY_CONFLICT | The idempotency key was already used by another flow |
MESSAGE_TEMPLATE_REQUIRED_INPUT_MAPPING_MISSING:<variable> | A required template variable has no input mapping on the message node (raised when publishing) |
MESSAGE_TEMPLATE_INPUT_MAPPING_UNKNOWN:<key> | The mapping key does not match any template variable key (raised when publishing) |
MESSAGE_TEMPLATE_INPUT_MAPPING_PATH_INVALID:<variable> | The mapping path is not an allowed runtime path (raised when publishing) |
RUNTIME_CONTEXT_PATH_NOT_FOUND:input.message.<variable> | The request body does not contain the message field the mapping reads (recorded in the log) |
MESSAGE_APP_CHANNEL_TARGETS_UNRESOLVED | The message node has no fixed recipients and no members resolved from the request body (recorded in the log) |
Message push flows are created and published like other flows: in the flow list, or with the OpenLink Runtime commands listed in the OpenLink system commands reference.
Recommended Workflow
- Create or select the OpenLink system that owns the notification
- Configure inbound authentication and whitelist rules for that system
- Create the message push flow with the Runtime command or in the flow list
- Open the design canvas, then add and configure the message node and, when the message needs analysis, an agent execution node before it
- Save, publish, and enable the flow
- Call
POST /api/open-link/message-push/<flowCode>from the external system and review the result in the execution logs