Skip to main content

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

  1. Concepts
  2. Organization Connections
  3. Partner Contact Configuration
  4. Publishing a Transmission
  5. Delivery Pipeline
  6. Receiving in a Workflow
  7. Attachments
  8. Replies and Correlation
  9. Status Lifecycle and Error Codes
  10. Redelivery
  11. Security and Isolation
  12. End-to-End Example
  13. Operational Notes
  14. Best Practices
  15. Related Topics

Concepts

TermMeaning
Organization connectionA 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 contactA 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.
TransmissionThe 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 / topicThere 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.
TransportHow 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.
  • isEnabled starts as true. 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"
}
}
KeyRequiredDescription
typeYesTransport discriminator. connection delivers through the organization connection. An unknown type is accepted on the contact but has no transport, so nothing is delivered.
organizationConnectionIdFor connectionThe 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.
organizationIdNoThe 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 contactId when 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 contactId on 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:

FieldBehaviour
channelOptional when contactId is set: derived from the contact's transmission.type, upper-cased (CONNECTION). An explicit value wins. Still required without a contact.
directionDefaults to Outbound.
correlationIdNew GUID when omitted.
payloadWritten 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.
statusDefaults 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

  1. The create command saves the outbound row, marks it Pending, and enqueues a delivery command in the transactional outbox.
  2. The outbox processor loads the row and its contact, parses the transmission block and picks the transport by type.
  3. The connection transport validates: the connection exists, is enabled, the sender is one of its two sides, and the optional organizationId matches the other side.
  4. Idempotency: if an inbound row already exists in the receiver with the same correlationId and customValues.sourceTransmissionId, delivery is treated as done.
  5. The transport resolves the receiver's reverse contact and, acting as the receiver organization's SYSTEM user, writes the mirrored inbound row.
  6. The outbound row becomes Sent (with startedAt, completedAt, durationMs). Rejections become Error with an error code. Unexpected exceptions become RetryScheduled and 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 fieldValue
direction / statusInbound / Received
sender / receiverThe sender's and receiver's organization uniqueId
contactIdThe receiver's reverse contact, or null when none carries the connection
customValues.organizationConnectionIdThe connection
customValues.sourceTransmissionIdThe outbound row's id (idempotency key; not resolvable across organizations)
orderIds, parentIdNot 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
Condition syntax

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:

VariableDescription
entityThe transmission: transmissionId, organizationId, direction, messageType, correlationId, contactId, channel, status, payloadRef, headers, customValues, and the two enrichments below.
entity.payloadThe 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.attachmentsThe attachment manifest with presigned url values, see Attachments.
entity.contactIdThe reverse contact, to reply against.
entity.customValues.organizationConnectionIdThe 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, data per entry. fileName is required for url and data; contentType defaults from the file name.
  • data entries 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.attachments and 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@1 to fetch the URL, then Attachment/Create@1 with fileData. Never pass a sender storage key to Attachment/Create fileUrl; 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

RowEventStatus
OutboundCreated with a deliverable contactPending
OutboundDelivery attempt startsunchanged, startedAt set
OutboundInbound row created (or already existed)Sent
OutboundTransient failure, outbox retry pendingRetryScheduled, retryCount incremented
OutboundRejected by validation or unknown transportError with errorCode
OutboundOutbox retries exhausted (dead-lettered)stays RetryScheduled; re-drive with redeliverTransmission
OutboundCreated without a deliverable contactyour status, unchanged from before
InboundCreatedReceived
errorCodeMeaning
ConnectionNotFoundThe contact block has no connection id, the connection does not exist, or the receiver organization is missing.
ConnectionDisabledThe connection's isEnabled is false.
SenderNotInConnectionThe sending organization is neither side of the connection.
OrganizationMismatchThe contact's organizationId GUID is not the other side of the connection.
UnknownTransportTypeThe 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.
  • contactId on updateTransmission is 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 Transmission entity 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 After triggers run inside the delivery save. If one throws, the sender's outbound row goes RetryScheduled, 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 an Async workflow 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 Pending row with no delivery; redeliverTransmission re-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

  1. Keep messageType values stable and namespaced by domain (order.created, ShipmentManifest), and write one workflow per message type on the receiving side.
  2. Set transmission.organizationId on partner contacts; it is a cheap guard against pointing a contact at the wrong connection.
  3. Reuse correlationId per business object so replies, updates and cancellations thread together.
  4. Write processing outcomes back to the inbound row (Transmission/Update@1), so both sides can see what happened in the transmissions grid.
  5. 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.
  6. Link orderIds when publishing so the order's transmission summary shows the exchange.
  7. Prefer attachmentId references for files that already exist; use data only for generated content.