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:
Content-Typecontainstext/xmlorapplication/soap+xml, and- a
SOAPActionheader is present, or the first 512 bytes of the body contain anEnvelopeelement.
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 |
$ 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-intaketenant: acmesoap_operation: SubmitOrdersoap_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-partnereffects: [http_egress]endpoint: https://partner.example.com/OrderServicemethod: POSTsoap_version: "1.1"soap_action: urn:partner:orders/GetOrderStatussoap_operation: GetOrderStatusread_timeout_ms: 10000The 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}/wsdlThe 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.
$ curl http://localhost:9090/flows/acme/order-intake/wsdlWhat 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.