Skip to content

SOAP

SOAP works in both directions: a flow can accept a SOAP request and answer with a SOAP response, and a flow can call a SOAP service. Envelope handling is automatic — you work with the body content.

Both SOAP 1.1 and 1.2 are supported, and envelope parsing is prefix-agnostic: <soap:Envelope>, <env:Envelope> and <Envelope> are all recognised.

Accepting SOAP

Nothing in the flow declares this. A request is treated as SOAP when it arrives on the ordinary /run route and both of these hold:

  1. Content-Type contains text/xml or application/soap+xml, and
  2. a SOAPAction header is present, or the first 512 bytes of the body contain an Envelope element.

The 512-byte window matters for one case only: an envelope preceded by a long preamble — comments, processing instructions, generous whitespace — is not recognised unless the caller also sends SOAPAction.

The envelope is unwrapped and the flow receives the body content as an XML value — not the envelope, not the header block.

Two variables are set:

Variable Contents
ctx.soap_action The SOAPAction header value, if any
ctx.soap_version 1.1 or 1.2
Terminal window
$ curl -X POST http://localhost:9090/flows/acme/order-intake/run \
-H "Authorization: Bearer $NEXUS_KEY" \
-H 'Content-Type: text/xml' \
-H 'SOAPAction: SubmitOrder' \
-d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SubmitOrder><orderId>A-1</orderId></SubmitOrder>
</soap:Body>
</soap:Envelope>'

The message is the <SubmitOrder> element, so $.orderId reads A-1: field access on an XML value returns the attribute of that name if there is one, and otherwise the first child element with that local name. See JSON and XML for the whole rule, including what comes back as text and what stays a node.

Answering SOAP

If the request was SOAP, the response is wrapped for you. Produce an XML value and it becomes the body of a correctly versioned envelope, with the matching content type:

Version Response content type
1.1 text/xml; charset=utf-8
1.2 application/soap+xml; charset=utf-8

A flow whose last step produces something other than XML still gets a valid envelope: the value is serialised as text inside the body, the content type is still correct, and the response carries X-Nexus-Soap-Warning: non-xml-output so the mismatch is visible rather than silent. Treat that header as a bug report about your flow.

A complete SOAP-to-SOAP flow

---
flowmarkdown_version: "0.1"
flow: order-intake
tenant: acme
soap_operation: SubmitOrder
soap_namespace: urn:acme:orders
---
Accepts a SOAP order submission and answers with an acknowledgement.
## Step: check
```validate
$["$tag"] == "SubmitOrder" | "unexpected operation" | syntactic
```
## Step: acknowledge
```ntd
<SubmitOrderResponse xmlns="urn:acme:orders">
<accepted>true</accepted>
<receivedAt>{{ date_format(now()) }}</receivedAt>
<correlationId>{{ ctx.correlation_id }}</correlationId>
</SubmitOrderResponse>
```

Calling a SOAP service

Add soap_version to an http_egress step. Its presence is what turns the call into a SOAP call.

Key Meaning
soap_version 1.1 or 1.2. Required to enable SOAP. Any other value is refused at publication
soap_action Value for the SOAPAction header
soap_operation Fallback for SOAPAction when soap_action is absent. It does not appear in the envelope
## Step: build-body
```ntd
<GetOrderStatus xmlns="urn:partner:orders">
<orderId>{{ $.orderId }}</orderId>
</GetOrderStatus>
```
## Step: call-partner
effects: [http_egress]
endpoint: https://partner.example.com/OrderService
method: POST
soap_version: "1.1"
soap_action: urn:partner:orders/GetOrderStatus
soap_operation: GetOrderStatus
read_timeout_ms: 10000

The message is wrapped in an envelope, the content type is set for the version, and the response envelope is unwrapped so the next step sees the body content.

Do not set content_type yourself on a SOAP step — it is derived from soap_version.

The version vocabulary is closed

soap_version accepts 1.1 and 1.2, and nothing else. A value outside that vocabulary is refused when the flow is published — by nexus deploy and by nexus validate alike — on ordinary steps, on ## Fault: steps, and inside a library expanded through call_flow or foreach. The refusal names every wrong step in one verdict, not just the first one:

step 'call-partner': `soap_version: 1.0` is not a SOAP version — write one of "1.1", "1.2"

Before this refusal existed, a misspelled version left a warning in the log and turned SOAP mode off: the step sent bare XML with no envelope, no SOAPAction and the ordinary content type, and the difference showed up only at the far end. An artifact published before the refusal still behaves that way, with a warning that names what the request became and asks for the flow to be republished. Republish such a flow and the typo becomes a refusal you can see.

Faults

When a SOAP request fails, the response is a SOAP fault with the correct envelope and content type. No configuration is needed.

The HTTP status of that fault depends on the envelope version, and the difference is deliberate.

Inbound envelope HTTP status of the fault
SOAP 1.1 500, whatever the error was
SOAP 1.2 the status the error means — 400 on a syntactic rule, 422 on a semantic one, 502 on a failed effect, 409 on a correlation conflict, 503 when state is unavailable, 500 otherwise

The 1.1 HTTP binding (§6.2) requires a fault to be carried on 500, so a 1.1 caller gets 500 and there is nothing to choose. SOAP 1.2 Part 2 (§7.5.2) permits any status, so a 1.2 caller gets the same number the same error produces on the JSON door — the platform has one error-to-status mapping and every door reads it.

Until this was fixed, both versions answered a flat 500, so the same validate rule answered 422 to a JSON caller and 500 to a SOAP caller. If your 1.1 client needs a status other than 500, write a ## Fault: section with response_status: — that is honoured on both versions.

A flow can choose the fault code and string by writing two variables before the failure:

Variable Effect
ctx.soap_fault_code Fault code
ctx.soap_fault_string Fault string

A ## Fault: section works as it does elsewhere, and its response_status: is honoured for SOAP responses too — so you can answer a validation failure with 400 and a fault envelope. What a SOAP branch does not pick up is the response-header set: it composes its own reply, so response_header_<name>: does not reach it. See Handling failures.

Serving a WSDL

GET /flows/{tenant}/{name}/wsdl

The document is generated from the flow’s front matter:

Front-matter key Meaning
soap_operation Operation name
soap_namespace Target namespace of the generated WSDL

Both keys or neither. There is no default for either one, and declaring only one is the same as declaring none: the flow has no SOAP configuration and this route answers 404. That is deliberate — an operation name guessed from the flow name, or a namespace invented by the platform, would appear in a contract handed to somebody else’s code generator.

The route needs no credentials, but it does need a visible tenant. A tenant that was never registered, or that has been disabled, gets the same 404 here as everywhere else, so a WSDL request cannot be used to find out which organisations exist on an installation.

Terminal window
$ curl http://localhost:9090/flows/acme/order-intake/wsdl

What the generated document contains: one service, one port, one operation, a SOAP 1.1 document/literal binding, and an address pointing at the flow’s /run endpoint.

What it does not contain: a schema for your messages. The message types are declared as xsd:anyType, because the platform does not know the shape of your payloads — it moves them. The WSDL is enough for a loosely typed client to discover the endpoint and the operation; a client that generates strongly typed stubs from a schema will not get useful types out of it. If the consumer needs a typed contract, hand them a hand-written schema alongside.

The binding is always declared as SOAP 1.1, even for a flow that accepts 1.2 requests.