Organization Connections and Transmissions
Organization connections link two organizations inside the same CXTMS instance. Transmissions carry messages, documents and files across such a link: one organization publishes an outbound transmission, the platform mirrors it into the partner organization as an inbound transmission, and the partner's workflows react to it. Together they replace point-to-point HTTP calls, shared API keys and file drops between tenants of the same platform.
This guide covers the whole path: creating a connection, configuring the partner contact, publishing, delivery, receiving in a workflow, attachments, replies and troubleshooting.
Table of Contents
- Concepts
- Organization Connections
- Partner Contact Configuration
- Publishing a Transmission
- Delivery Pipeline
- Receiving in a Workflow
- Attachments
- Replies and Correlation
- Status Lifecycle and Error Codes
- Redelivery
- Security and Isolation
- End-to-End Example
- Operational Notes
- Best Practices
- Related Topics
Concepts
| Term | Meaning |
|---|---|
| Organization connection | A row linking two organizations (sourceOrganization, targetOrganization) with an application-defined connectionType, owned by a third organization (usually System Administration) whose workflows react to the link. |
| Partner contact | A regular Contact in each organization that represents the other organization. Its custom values carry a transmission block that tells the platform how to reach the partner. |
| Transmission | The existing Transmission entity. A cross-organization message is two rows: an Outbound row in the sender and a mirrored Inbound row in the receiver. |
| Channel / topic | There is no channel table. The topic of a message is its messageType; the enabled connection is the only gate; the receiver subscribes by having a workflow with a trigger on Transmission that filters on messageType. |
| Transport | How an outbound transmission is delivered. The contact's transmission.type selects it. connection is the implemented transport; the same block is designed to carry other types (for example SFTP) later. |
Organization Connections
Creating and removing a connection
Connections are created and removed by system administrators through GraphQL. The platform stores only the pair and the type; what a connectionType means is up to the application that owns it.
mutation Link {
linkOrganizations(input: {
organizationId: 1 # owner: the organization whose workflows react to the link
sourceOrganizationId: 8
targetOrganizationId: 2
connectionType: "poolPointCustomer"
}) {
organizationConnection { organizationConnectionId isEnabled connectionType }
}
}
mutation Unlink {
unlinkOrganizations(input: { organizationConnectionId: 12 }) {
deleteResult { rowsAffected }
}
}
query Connections {
organizationConnections(organizationId: 1, search: "Harbor", take: 20, skip: 0) {
items {
organizationConnectionId
connectionType
isEnabled
sourceOrganization { organizationId companyName }
targetOrganization { organizationId companyName }
}
totalCount
}
}
Rules enforced by the platform:
- Both linked organizations must be regular organizations, and an organization cannot be linked to itself.
- One connection per pair and
connectionType, in either order. isEnabledstarts astrue. A disabled connection rejects every delivery (see Status Lifecycle and Error Codes).- Only members of the System Administration organization can link or unlink.
Reacting to a connection with workflows
OrganizationConnection fires entity triggers in the owner organization (organizationId of the link) for Added, Modified and Deleted, at both Before and After positions. This is where an application creates the counterparty contacts on both sides, mirrors master data, and stamps the transmission configuration described below. A Before / Deleted workflow that throws vetoes the unlink.
workflow:
name: "Link pool point and customer"
executionMode: Sync
runAs: SYSTEM
triggers:
- type: Entity
entityName: OrganizationConnection
eventType: Added
position: After
conditions:
- expression: "[entity.connectionType] = 'poolPointCustomer'"
The entity variable holds organizationConnectionId, organizationId (owner), sourceOrganizationId, targetOrganizationId, connectionType, isEnabled and ediRoutingId.
Partner Contact Configuration
Every organization that sends or receives over a connection needs a contact that represents the partner. The contact is ordinary (any contact type, any name); what makes it a transmission target is the transmission block in its custom values:
{
"transmission": {
"type": "connection",
"organizationConnectionId": 12,
"organizationId": "3f9a6c1e-0b7d-4a8e-9c2d-5e1f7b8a9d01"
}
}
| Key | Required | Description |
|---|---|---|
type | Yes | Transport discriminator. connection delivers through the organization connection. An unknown type is accepted on the contact but has no transport, so nothing is delivered. |
organizationConnectionId | For connection | The connection to deliver through. Store it as a number; a string value parses on the sender side but does not match the reverse-contact lookup on the receiver side. |
organizationId | No | The partner organization's uniqueId (GUID). When present, delivery checks it against the other side of the connection and rejects a mismatch. |
Set the block on both sides:
- In the sender, on the contact that represents the receiver. This is the contact you pass as
contactIdwhen publishing. - In the receiver, on the contact that represents the sender. The platform looks it up (first non-deleted contact whose block names the same connection) and sets it as
contactIdon the inbound row, so the receiver can reply against it.
The natural place to set both blocks is the OrganizationConnection / Added workflow shown above, using Contact/Create@1 in each organization.
Publishing a Transmission
Publishing uses the existing Transmission/Create@1 task or the createTransmission GraphQL mutation. A transmission becomes deliverable when it is Outbound, has a contactId, and that contact's transmission.type has a registered transport.
- task: "Transmission/Create@1"
name: publish
inputs:
organizationId: "{{ organizationId }}"
transmission:
contactId: "{{ partnerContactId }}"
messageType: "order.created"
correlationId: "{{ correlationId }}" # optional; reuse to reply in-thread
orderIds: # optional; links the sender's own orders
- "{{ orderId }}"
payload: "{{ order }}" # object, text, bytes or stream
attachments: # optional, see Attachments
- attachmentId: "{{ bolAttachmentId }}"
metadata: { documentType: BOL }
outputs:
- name: transmission
mapping: "transmission"
What the platform fills in:
| Field | Behaviour |
|---|---|
channel | Optional when contactId is set: derived from the contact's transmission.type, upper-cased (CONNECTION). An explicit value wins. Still required without a contact. |
direction | Defaults to Outbound. |
correlationId | New GUID when omitted. |
payload | Written to storage under transmissions/{organizationId}/{yyyy}/{MM}/{correlationId}/{guid}.{ext}; payloadRef and byteSize are set and headers["Content-Type"] is added when absent. Objects become application/json, strings text/plain, bytes application/octet-stream. payloadContentType overrides the guess (for example application/edi-x12). payload and a caller-supplied payloadRef are mutually exclusive. |
status | Defaults to Pending for a deliverable row when you do not set one. |
In a workflow, payload may be a JSON object, a text string, a byte[] or a Stream from a previous task (a generated PDF, a downloaded file). A string is treated as text unless payloadContentType names a binary type, in which case it is base64-decoded. Over GraphQL, payload is any JSON value, or a base64 string together with a binary payloadContentType.
The same call over GraphQL:
mutation Publish {
createTransmission(input: {
organizationId: 8
values: {
contactId: 4711
messageType: "order.created"
payload: { orderId: 4242, trackingNumber: "1Z999" }
}
}) {
transmission { transmissionId status correlationId payloadRef }
}
}
A transmission created without a contact behaves exactly as before this feature: it is an audit row for an external exchange, nothing is uploaded or delivered.
Delivery Pipeline
- The create command saves the outbound row, marks it
Pending, and enqueues a delivery command in the transactional outbox. - The outbox processor loads the row and its contact, parses the
transmissionblock and picks the transport bytype. - The
connectiontransport validates: the connection exists, is enabled, the sender is one of its two sides, and the optionalorganizationIdmatches the other side. - Idempotency: if an inbound row already exists in the receiver with the same
correlationIdandcustomValues.sourceTransmissionId, delivery is treated as done. - The transport resolves the receiver's reverse contact and, acting as the receiver organization's SYSTEM user, writes the mirrored inbound row.
- The outbound row becomes
Sent(withstartedAt,completedAt,durationMs). Rejections becomeErrorwith an error code. Unexpected exceptions becomeRetryScheduledand the outbox retries with backoff, dead-lettering after its configured attempts.
The inbound row copies channel, messageType, correlationId, payloadRef, byteSize, headers, protocol and customValues, and adds:
| Inbound field | Value |
|---|---|
direction / status | Inbound / Received |
sender / receiver | The sender's and receiver's organization uniqueId |
contactId | The receiver's reverse contact, or null when none carries the connection |
customValues.organizationConnectionId | The connection |
customValues.sourceTransmissionId | The outbound row's id (idempotency key; not resolvable across organizations) |
orderIds, parentId | Not copied. Order ids and parent links do not cross organizations. |
Receiving in a Workflow
Transmission fires entity triggers (Added, Modified, Deleted, Before and After) in the organization that owns the row. Inserting the inbound row is therefore all the receiver needs: subscribe with an entity trigger and filter on direction and topic.
workflow:
name: "Process inbound orders"
executionMode: Sync
runAs: SYSTEM
triggers:
- type: Entity
entityName: Transmission
eventType: Added
position: After
conditions:
- expression: "[entity.direction] = 'Inbound' && [entity.messageType] = 'order.created'"
activities:
- name: Process
steps:
- task: "Order/Import@1"
name: importOrder
inputs:
organizationId: "{{ organizationId }}"
order: "{{ entity.payload }}"
- task: "Transmission/Update@1"
name: markProcessed
inputs:
organizationId: "{{ organizationId }}"
transmissionId: "{{ entity.transmissionId }}"
transmission:
status: Acknowledged
Trigger conditions are NCalc expressions. Variable paths must be written in square brackets, [entity.messageType]; bare dotted paths do not parse. Without the direction condition the workflow also fires for the organization's own outbound rows.
Variables available in the workflow:
| Variable | Description |
|---|---|
entity | The transmission: transmissionId, organizationId, direction, messageType, correlationId, contactId, channel, status, payloadRef, headers, customValues, and the two enrichments below. |
entity.payload | The decoded payload: an object for JSON, a string for text, bytes otherwise. Loaded only when at least one workflow subscribes and byteSize is within Transmissions:PayloadPreloadLimitBytes (default 50 MB); otherwise null, and the workflow reads entity.payloadRef with the file tasks. |
entity.attachments | The attachment manifest with presigned url values, see Attachments. |
entity.contactId | The reverse contact, to reply against. |
entity.customValues.organizationConnectionId | The connection the message arrived through. |
Both organizations see their own rows in the transmissions grid, with contact, payloadRef and attachments exposed through GraphQL.
Attachments
Files ride as a manifest, not as copies. Each entry references an existing attachment in the sender organization, an external URL, or raw data:
attachments:
- attachmentId: 4711 # existing attachment in the sender
metadata: { documentType: BOL }
- attachmentGuid: "…" # alternative reference
- url: "https://…" # any URL the sender already has
fileName: label.pdf
contentType: application/pdf
- data: "{{ pdfStream }}" # bytes / stream (workflow) or base64 (GraphQL)
fileName: invoice.pdf
contentType: application/pdf
metadata: { invoiceNumber: "INV-1" }
- Exactly one of
attachmentId,attachmentGuid,url,dataper entry.fileNameis required forurlanddata;contentTypedefaults from the file name. dataentries are uploaded under the transmission's storage prefix. Referenced attachments are not copied; if the sender deletes the attachment later, the receiver's reference stops working.- The manifest is stored in
customValues.attachmentsand mirrored to the inbound row. - When the receiver's trigger fires, and on GraphQL
transmission.attachments, every storage key is replaced by a presigned download URL (24 hours). Keys are presigned only when they belong to the row's own organization or, for inbound rows, the partner organization on the connection; anything else is refused and logged. - To keep a file, the receiver downloads it and creates its own attachment:
Utilities/HttpRequest@1to fetch the URL, thenAttachment/Create@1withfileData. Never pass a sender storage key toAttachment/CreatefileUrl; that path moves the object inside the bucket.
Replies and Correlation
A reply is an ordinary publish from the receiver against its reverse contact, reusing the incoming correlationId:
- task: "Transmission/Create@1"
name: reply
inputs:
organizationId: "{{ organizationId }}"
transmission:
contactId: "{{ entity.contactId }}"
messageType: "order.accepted"
correlationId: "{{ entity.correlationId }}"
payload: { orderId: "{{ entity.payload.orderId }}", accepted: true }
Both organizations can then follow the whole exchange by correlationId. A stable correlation id per business object (an import run, a delivery order) keeps every later update and cancellation in one thread.
Status Lifecycle and Error Codes
| Row | Event | Status |
|---|---|---|
| Outbound | Created with a deliverable contact | Pending |
| Outbound | Delivery attempt starts | unchanged, startedAt set |
| Outbound | Inbound row created (or already existed) | Sent |
| Outbound | Transient failure, outbox retry pending | RetryScheduled, retryCount incremented |
| Outbound | Rejected by validation or unknown transport | Error with errorCode |
| Outbound | Outbox retries exhausted (dead-lettered) | stays RetryScheduled; re-drive with redeliverTransmission |
| Outbound | Created without a deliverable contact | your status, unchanged from before |
| Inbound | Created | Received |
errorCode | Meaning |
|---|---|
ConnectionNotFound | The contact block has no connection id, the connection does not exist, or the receiver organization is missing. |
ConnectionDisabled | The connection's isEnabled is false. |
SenderNotInConnection | The sending organization is neither side of the connection. |
OrganizationMismatch | The contact's organizationId GUID is not the other side of the connection. |
UnknownTransportType | The contact's transmission.type has no registered transport. |
Rejections are final and are not retried. Only unexpected exceptions go through the outbox retry path.
Redelivery
redeliverTransmission(organizationId, transmissionId) resets an outbound row to Pending, clears its error fields and enqueues delivery again. Use it after fixing a disabled connection or a wrong contact block, or when a row is stuck at Pending with no outbox message (a crash between the two saves of the create command). Delivery is idempotent, so redelivering a row that was already mirrored is harmless. Only outbound rows with a deliverable contact can be redelivered.
Security and Isolation
- Publishing needs nothing beyond access to the sender organization. The contact must belong to that organization, and it is the only thing that can name a connection.
- The connection is the gate: disabled, or not including the sender, means no side effect in the receiver.
- The inbound row is written as the receiver organization's SYSTEM user; the sender's user never acts inside the receiver. The sender's user context is restored after delivery.
- The receiver never sees the sender's contact id, order ids or parent link, only
sourceTransmissionId, which it cannot dereference. contactIdonupdateTransmissionis validated against the organization, so a transmission cannot be pointed at another tenant's contact.- Payload objects live under the sender's storage prefix; presigned URLs are the only way they leave the platform, and only for keys the row's own or partner organization owns.
End-to-End Example
Two organizations, a customer (id 8) and a pool point (id 2), linked with connectionType: poolPointCustomer (connection 12). Each has a contact for the other carrying the block from Partner Contact Configuration.
Customer: publish a manifest after import
workflow:
name: "Disseminate manifest"
executionMode: Async
triggers:
- type: Manual
activities:
- name: Send
steps:
- task: "Transmission/Create@1"
name: sendManifest
inputs:
organizationId: "{{ organizationId }}"
transmission:
contactId: "{{ poolPointContactId }}"
messageType: "ShipmentManifest"
correlationId: "{{ importRunId }}"
orderIds: "{{ orderIds }}"
payload: "{{ cartons }}"
Pool point: receive it
workflow:
name: "Receive manifest"
executionMode: Sync
runAs: SYSTEM
triggers:
- type: Entity
entityName: Transmission
eventType: Added
position: After
conditions:
- expression: "[entity.direction] = 'Inbound' && [entity.messageType] = 'ShipmentManifest'"
activities:
- name: Import
steps:
- task: "Foreach@1"
name: eachCarton
inputs:
items: "{{ entity.payload }}"
steps:
- task: "Order/Import@1"
name: importCarton
inputs:
organizationId: "{{ organizationId }}"
order: "{{ item }}"
- task: "Transmission/Update@1"
name: acknowledge
inputs:
organizationId: "{{ organizationId }}"
transmissionId: "{{ entity.transmissionId }}"
transmission:
status: Acknowledged
After the customer's workflow runs: the customer's row goes Pending then Sent; the pool point has one Inbound / Received row with the same correlationId, contactId set to its Customer contact, and its workflow has imported the cartons and acknowledged the row.
Operational Notes
- Every organization now gets
Transmissionentity triggers, including for transmissions it already creates for EDI, API or webhook audit. Scope subscriptions with[entity.direction]and[entity.messageType]conditions. - A failing receiver workflow reaches the sender. The receiver's
Aftertriggers run inside the delivery save. If one throws, the sender's outbound row goesRetryScheduled, the outbox retries, and the message eventually dead-letters. Idempotency prevents duplicate inbound rows on retry. Keep receiver workflows defensive, or move risky processing to anAsyncworkflow triggered by the same event. - Two saves on create. The outbox message needs the generated transmission id, so the create command saves twice. A crash in between leaves a
Pendingrow with no delivery;redeliverTransmissionre-drives it. - Presigned URLs expire after 24 hours. Receivers that need a file must import it when the message arrives.
- Payload preload limit is configurable per server as
Transmissions:PayloadPreloadLimitBytes.
Best Practices
- Keep
messageTypevalues stable and namespaced by domain (order.created,ShipmentManifest), and write one workflow per message type on the receiving side. - Set
transmission.organizationIdon partner contacts; it is a cheap guard against pointing a contact at the wrong connection. - Reuse
correlationIdper business object so replies, updates and cancellations thread together. - Write processing outcomes back to the inbound row (
Transmission/Update@1), so both sides can see what happened in the transmissions grid. - Mark inbound-originated changes on the entities you update (for example a custom value with the inbound transmission id) and exclude them from your own outbound triggers to avoid echo loops on two-way syncs.
- Link
orderIdswhen publishing so the order's transmission summary shows the exchange. - Prefer
attachmentIdreferences for files that already exist; usedataonly for generated content.
Related Topics
- Transmission Entity
- Organization Connection Entity
- Transmission Tasks
- Workflow Triggers
- Attachment Tasks
- Workflow Webhook for exchanges with systems outside the platform