Workflow Integration
Workflow integration lets a workflow call tenant-side domain methods, execute an OpenLink service-call flow, or wait for an external system to return an asynchronous result. The Approval Process and Automation Task design canvases use the same contract; only the payload source and the runtime differences below change.
| Workflow | Design canvas | Payload source | Callback difference |
|---|---|---|---|
| Approval process | Approval process design canvas | source.type = 'approval' with the instance, node, trigger, history, and callback result | The wait callback node has no timeout; the instance waits until the callback arrives |
| Automation task | Automation task design canvas | source.type = 'automation' with the definition, execution, node, trigger snapshot, and step output | The wait callback node requires a timeout (at least 60 when the unit is seconds) and the execution fails when it expires |
It is not a public Runtime API. The actual integration entry points are the Service Call and Wait Callback nodes on either design canvas:
| Scenario | Node sequence | Integration method |
|---|---|---|
| Call an internal Soln capability | Service Call (Studio script) | Call Studio Runtime methods such as context.form.* and context.http.* |
| Call an external system synchronously | Service Call (integration flow) | Execute a published inbound or outbound OpenLink flow whose trigger is Service Call |
| Wait for an asynchronous external result | Service Call → Wait Callback | Start the external task, then complete the callback through an inbound OpenLink Webhook flow |
Tenant data and integration execution records remain tenant-side. External systems use the public entry point configured by OpenLink and never call internal Runtime routes.
Prerequisites
Open the target workflow and enter its design canvas: a process from Approval Process Management, or a task from Automation Tasks. Before configuring an integration, confirm that:
- The workflow is bound to the correct local form
- A Studio script asset and entry file are published before they are selected
- An OpenLink flow, its form binding, and an active flow version are enabled and published
- A service-call flow uses the same local form as the workflow
- An asynchronous callback flow is a published inbound Webhook flow using that same local form
Service-Call Payload
Studio scripts and OpenLink service-call flows receive the same soln.runtime-context/v1 context, and the node inspector shows a payload preview for the current workflow.
An approval process sends the approval source:
type ApprovalServiceCallPayload = {
schema: 'soln.runtime-context/v1';
source: {
type: 'approval';
definitionId: string;
instanceId: string;
nodeKey: string;
nodeName: string;
trigger: {
action: 'create' | 'update' | 'delete';
changedFields: string[];
sourceDataStatus: string | null;
sourceApprovalStatus: string | null;
inheritedOutcome: 'approved' | 'rejected' | 'returned' | null;
};
history: Array<{
nodeKey: string;
nodeName: string;
nodeType: string;
status: string;
outcome: string | null;
detail: Record<string, unknown>;
startedAt: string;
completedAt: string | null;
}>;
callback: {
flowDefinitionId: string | null;
correlationKey: string | null;
outcome: 'approved' | 'rejected' | 'returned' | null;
payload: Record<string, unknown>;
receivedAt: string | null;
} | null;
};
form: {
id: string;
apiName: string;
name: string | null;
} | null;
record: {
id: string;
data: Record<string, unknown>;
system: Record<string, unknown>;
} | null;
runtime: {
locale: string;
timezone: string;
executionId: string;
traceId: string;
};
};
An automation task sends the automation source:
type AutomationServiceCallPayload = {
schema: 'soln.runtime-context/v1';
source: {
type: 'automation';
definitionId: string;
executionId: string;
nodeKey: string;
trigger: Record<string, unknown>;
steps: Record<string, { output: unknown }>;
};
form: { id: string; apiName: string; name: string | null } | null;
record: { id: string; data: Record<string, unknown> } | null;
result: unknown;
runtime: {
executionId: string;
};
};
Fields both workflows share:
| Field | Purpose |
|---|---|
record.id | ID of the record the workflow is working on; use it with record-targeted domain methods |
record.data | Record data snapshot when the service-call node runs, keyed by field API name |
form.apiName | API name of the related form; use it with form-targeted methods |
source.nodeKey | Current service-call node key |
runtime.traceId | Trace ID for this node execution; suitable as an external idempotency key |
Approval-only fields:
| Field | Purpose |
|---|---|
source.instanceId | Current approval instance ID |
source.trigger | Record action and changed-field information that started the approval |
source.history | Execution history before the current node |
source.callback | Latest callback result when an upstream wait callback has completed |
Automation-only fields:
| Field | Purpose |
|---|---|
source.definitionId | Automation task definition ID |
source.executionId | Current automation execution ID |
source.trigger | Snapshot of the trigger configuration |
source.steps | Step output of the nodes that already ran |
result | Upstream node result |
Both payloads include the complete record.data snapshot. Do not find the current record through URL parameters.
Call Internal Domain Methods
Configure The Service-Call Node
- Add a Service Call node to the design canvas
- Set Call Method to Studio Script
- Select a published script asset and entry file
- Inspect the payload preview and verify every field API name used by the script
- Connect exactly one incoming and one outgoing route
- Save and publish the workflow
“Internal methods” are tenant domain methods exposed by Studio Runtime, such as context.form.get, context.form.update, and context.form.create. A script must not call legacy MCP tools or connect directly to the tenant database.
Example: Update The Current Record After Approval
type StudioRuntimeContext = {
form: {
update(input: {
recordId: string;
data: Record<string, unknown>;
}): Promise<unknown>;
};
};
export default async function main(
payload: ApprovalServiceCallPayload,
context: StudioRuntimeContext,
) {
if (!payload.record?.id) {
return {
ok: false,
message: 'Approval record is unavailable.',
};
}
if (payload.source.trigger.inheritedOutcome !== 'approved') {
return { ok: true, skipped: true };
}
await context.form.update({
recordId: payload.record.id,
data: {
integrationStatus: 'approved',
approvalInstanceId: payload.source.instanceId,
},
});
return {
ok: true,
recordId: payload.record.id,
};
}
Any of these return values marks the service call as failed and blocks the approval instance at this node:
return { ok: false, message: 'Unable to update the record.' };
return { success: false, errorMessage: 'External validation failed.' };
A status of failed, failure, error, or rejected is also treated as failure. A thrown exception, script execution failure, or unavailable published entry file blocks the node as well. The execution log records the reason, and the service call can be retried after the issue is resolved.
Example: Call An External HTTP Service From A Script
A small synchronous integration can call context.http.requestJson directly:
type StudioRuntimeContext = {
http: {
requestJson<T>(input: {
method: string;
url: string;
headers?: Record<string, string>;
json?: unknown;
timeoutMs?: number;
}): Promise<{ status: number; data: T }>;
};
};
export default async function main(
payload: ApprovalServiceCallPayload,
context: StudioRuntimeContext,
) {
if (!payload.record) {
return { ok: false, message: 'Approval record is unavailable.' };
}
const response = await context.http.requestJson<{
accepted: boolean;
referenceId?: string;
}>({
method: 'POST',
url: 'https://api.example.com/risk-checks',
headers: {
'Idempotency-Key': payload.runtime.traceId,
},
json: {
recordId: payload.record.id,
amount: payload.record.data.amount,
approvalInstanceId: payload.source.instanceId,
},
timeoutMs: 10_000,
});
if (!response.data.accepted) {
return {
ok: false,
message: 'Risk check was rejected.',
};
}
return {
ok: true,
referenceId: response.data.referenceId ?? null,
};
}
Do not hard-code production credentials in scripts. Configure external authentication through the OpenLink integration system. Prefer an OpenLink service-call flow when the integration needs reusable mapping, authentication, and execution auditing.
Execute An OpenLink Flow From A Service-Call Node
Prepare A Service-Call Flow
- Open Integration Flows under the target OpenLink
- Create an inbound or outbound flow
- Set its trigger to Service Call
- Select the same local form used by the approval process
- Configure source, transform, target, and optional message nodes
- Save, publish, and enable the flow
The approval service-call selector only includes flows that:
- are published and have an active version;
- use the Service Call trigger;
- use the same local form as the approval process; and
- are inbound or outbound OpenLink flows
Configure The Node
- Add a Service Call node on the design canvas
- Set Call Method to Integration Flow
- Select the published flow from the form-grouped options
- Connect the incoming and outgoing routes, then save and publish the workflow
At runtime, the node sends the full payload to OpenLink and waits for that service-call execution to finish:
- Success completes the node and continues the workflow
- Failure blocks the node and the workflow run
- Retry runs the same execution and node again; it does not create another path
Example: Send A Purchase Order Synchronously
Approval canvas:
Start → Approval → Service Call (flow: Send Purchase Order) → End
Outbound OpenLink flow:
Start → Source → Transform → Target (Studio script/external call) → End
The target reads the purchase order from the runtime record and calls the external system. That system must return its final success or failure during the current execution. If the final result will arrive later, use a Service Call followed by Wait Callback instead of holding a synchronous call open.
Wait For An Approval Callback
When To Use It
Use the following sequence when an external system accepts a job first and produces a final result after manual review, asynchronous processing, or third-party work:
Service Call → Wait Callback → Next node
Wait Callback must be directly connected after Service Call. Do not place a condition, message, or approval node between them.
Prepare The Callback Flow
- Create an Inbound OpenLink flow
- Set its trigger to Webhook
- Use the same local form as the approval process
- Set its target action to Complete Workflow Callback
- Save, publish, and enable the flow
- Copy the Webhook URL shown by the flow
The callback flow must be a published inbound Webhook flow with an active version and enabled binding. Do not use a Service Call flow as the callback endpoint.
Configure The Wait-Callback Node
- Connect Wait Callback directly after the node that starts the asynchronous job
- Select the inbound Webhook flow prepared above
- Configure a correlation-key template. A common value is
{{data.record.id}} - Pass the rendered key to the external system and require it to return the same key
- Connect exactly one outgoing route
- Save and publish the workflow
An approval wait callback has no timeout: the instance waits until the callback arrives. An automation wait callback always has a timeout (at least 60 when the unit is seconds), and the wait node plus that execution fail when it expires.
The key matches a pending wait inside the current tenant and callback flow. It must be stable and identify exactly one wait. If the same record can have concurrent waiting approvals, use a key that includes the approval instance or external task identity, using only template values supported by the inspector.
Example: Start An Asynchronous Job
A Service Call node can create the external task through a Studio script:
export default async function main(
payload: ApprovalServiceCallPayload,
context: StudioRuntimeContext,
) {
if (!payload.record) {
return { ok: false, message: 'Approval record is unavailable.' };
}
const correlationKey = payload.record.id;
const response = await context.http.requestJson<{
taskId: string;
}>({
method: 'POST',
url: 'https://api.example.com/manual-reviews',
headers: {
'Idempotency-Key': payload.runtime.traceId,
},
json: {
correlationKey,
recordId: payload.record.id,
callbackUrl: 'https://soln.example.com/api/open-link/webhooks/review-callback',
},
timeoutMs: 10_000,
});
return {
ok: true,
correlationKey,
externalTaskId: response.data.taskId,
};
}
The correlationKey sent by the script must equal the key rendered by the Wait Callback template. Use the actual Webhook URL shown in the node inspector; do not copy the example URL.
External Callback Request
After processing succeeds, the external system sends this JSON to the callback flow Webhook URL:
{
"callbackId": "review-result-8f1d31",
"correlationKey": "record-7c831b",
"status": "succeeded",
"result": {
"decision": "approved",
"externalTaskId": "review-2048",
"comment": "Risk review passed"
},
"error": null,
"completedAt": "2026-09-10T08:30:00.000Z"
}
| Field | Required | Description |
|---|---|---|
callbackId | Yes | External request identifier for this callback. Generate a unique value |
correlationKey | Yes | Must exactly match the rendered Wait Callback key |
status | Yes | Must be succeeded for an approval callback |
result | Yes | Callback result object |
result.decision | No | approved, rejected, or returned |
error | Yes | Must be null on success |
completedAt | No | Valid ISO date-time or null |
Approval callbacks do not accept failed or canceled. If the external job fails, keep it available for retry or encode a routable business outcome in the result of a successful callback according to the integration contract.
A matching callback completes the wait node and resumes the approval. No match produces WORKFLOW_CALLBACK_WAIT_NOT_FOUND; more than one pending match for the same flow and key produces WORKFLOW_CALLBACK_WAIT_NOT_UNIQUE.
Route Using The Callback Outcome
A later service-call or message node receives the most recent result in source.callback:
export default async function main(payload: ApprovalServiceCallPayload) {
const callback = payload.source.callback;
if (callback?.outcome === 'rejected') {
return {
ok: false,
message: String(callback.payload.comment || 'External review rejected.'),
};
}
return {
ok: true,
externalTaskId: callback?.payload.externalTaskId ?? null,
};
}
To branch on the canvas, add a Branch after Wait Callback and select the Callback outcome field:
Service Call → Wait Callback → Branch (callback result)
├─ approved → next processing
└─ rejected / returned → alternate processing
Callback outcome is available only when the branch's upstream path has passed through a Wait Callback node.
Automation tasks branch with a Branch node instead: its condition reads record fields, and the callback output is available to later nodes through source.steps.
Complete Integration Examples
Synchronous Integration
Start
→ Approval
→ Service Call (Studio script or service-call OpenLink flow)
→ Message
→ End
Use this when the external service returns its final result in one execution. Failure blocks the approval instance until the issue is fixed and the node is retried.
Asynchronous Approval Integration
Start
→ Approval
→ Service Call (create external job and send correlationKey)
→ Wait Callback (inbound Webhook + workflow_callback)
→ Branch (context.callback.outcome)
├─ approved → update record → End
├─ rejected → Message → End
└─ returned → Human processing → End
In this sequence:
- Service Call only creates the external job
- Wait Callback persists the pending wait and pauses the approval
- The external system calls the OpenLink Webhook
- OpenLink validates the callback contract and matches the wait
- The approval resumes and exposes the callback result to later nodes
Automation Task Examples
Synchronous Automation Call
Start (data change: order created)
→ Service call (studio script: call the external system)
→ Data processing (update the order)
→ End
Use this when the script or flow returns its final result in the same execution. A failure blocks the node and the automation execution until the issue is fixed and the node is retried.
Asynchronous Automation Callback
Start (data change: order created)
→ Service call (studio script: create the external review and send correlationKey)
→ Wait callback (inbound Webhook with a timeout)
→ Data processing (write the callback outcome to the record)
→ End
- The service call only creates the external job
- The wait callback stores the pending wait and pauses the execution; its timeout must be valid, at least 60 when the unit is seconds
- The external system calls the OpenLink Webhook with the callback contract
- The execution resumes, and later nodes read the earlier step output from
source.stepsor write the result back with a data processing node
Failure Handling And Idempotency
- Use
runtime.traceIdor a stable business key as an external idempotency key - Generate a unique external
callbackIdfor each callback request correlationKeymust exactly match the pending wait and must not match multiple waits- A Studio return with
ok: false,success: false, or a failurestatusblocks the workflow - Failed OpenLink service-call executions also block the workflow and preserve execution logs
- External systems must not call internal Runtime routes; the public callback entry point is the OpenLink Webhook
- Do not use legacy MCP tools or direct tenant-database access from service-call scripts
- Update tenant business records through Studio Runtime domain methods using the real
record.id - Use synchronous calls only for bounded work; use Wait Callback for long-running work
- Before publishing, test field API names, the correlation key, external authentication, callback payload, and failure routes with real tenant records