SOAP
Ingest a legacy SOAP service (Siebel, SAP PI) by putting the
<soap:Body>in the manifest — a request shape on the REST engine (connector_typestaysrest-generic), with QueryPage pagination.
| Type | rest-generic (shape soap) |
| Protocol | SOAP 1.1 over POST |
| Target | ext.* hybrid |
| Schedulable | cron |
| Edition | Open |
Auth, retry, rate limiting and the hybrid JSONB + typed storage are all shared with REST. This page covers what is different: the body template, QueryPage pagination, and the error channel that is not the HTTP status.
Quickstart
Every screen on this page reads a demo service that ships with the repo
The same Python file the REST page uses answers SOAP on POST /<service>/start.swe — eight
services over the same 2 400 fictional ACME partners, in XML. Start it in a terminal of its
own, and put the credential these manifests need in the hub's registry:
python3 docs/connectors/rest/demo/acme-api.py
lm secret set ACME_BASIC_PW # demo-pass
The manifests point at http://host.docker.internal:8099, because the hub runs in a
container and localhost inside it is that container. What each service imitates, and
which ones misbehave on purpose: The demo API.
# 90-soap-querypage.yaml — the Siebel QueryPage shape, on the REST connector.
#
# `connector_type` stays rest-generic. `kind: soap` sends a SOAP envelope by POST; the page
# offset goes inside the body. `kind: soap` and `pagination.kind: soap` go together: one
# without the other is refused at apply.
#
# The body's placeholders: {{page_size}}, {{cursor}} (row offset from 0), {{page}} (page
# number from 1), {{secret:NAME}}, {{env:NAME}}.
#
# Write paths without namespace prefixes (Customer, not ns:Customer). `response_path` is a
# plain dotted path to the repeated element: no [*], no `..`.
#
# XML has no types: `promote` gives each column its type.
#
# lm secret set ACME_BASIC_PW # the demo API answers to demo-user / demo-pass
#
# Expect: 2 400 rows in ext.acme_soap_partners, read 500 at a time.
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: acme-soap
connector_type: rest-generic
scopes: [billing]
spec:
base_url: http://host.docker.internal:8099
auth:
kind: basic
username: demo-user
password_env: ACME_BASIC_PW
endpoints:
- id: partners
kind: soap
path: /siebel/start.swe
soap_action: "document/urn:acme:CustomerQueryPage"
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
<ListOfCustomer><Customer><Id query="*"/><Name query="*"/></Customer></ListOfCustomer>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 500
last_page_path: "$.Body.CustomerQueryPage_Output.LastPage"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_partners
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
- { name: city, path: "$.City", type: text }
- { name: credit_limit, path: "$.CreditLimit", type: decimal }
- { name: updated_at, path: "$.UpdatedAt", type: timestamptz, transform: parse_iso8601 }
$ lm validate -f docs/connectors/rest/demo/90-soap-querypage.yaml
✓ 90-soap-querypage.yaml — valid
$ lm apply -f docs/connectors/rest/demo/90-soap-querypage.yaml
Applied connector "acme-soap" — 1 endpoint
Before running anything, send the first request a run would send:
$ lm source rest test acme-soap
ENDPOINT METHOD STATUS ITEMS VERDICT
partners POST 200 500 ok
The pre-flight sends the request a run sends — envelope, body template, SOAPAction — for one
page. Nothing is written.
$ lm connector run acme-soap
run scheduled: 45b9d0b8-ffb9-4bbf-ab2a-95fe42e03d70
$ lm run list --connector acme-soap
ID CONNECTOR ENDPOINT STATUS IN OUT SKIPPED DEAD-LETTERS STARTED ENDED
45b9d0b8 acme-soap partners Completed 2400 2400 0 0 2026-10-19 01:46 2026-10-19 01:46
What landed in ext.acme_soap_partners — the promoted columns, beside the whole payload:
lumnik=> SELECT external_id, name, city, credit_limit, updated_at
lumnik-> FROM ext.acme_soap_partners ORDER BY external_id LIMIT 3;
external_id | name | city | credit_limit | updated_at
-------------+---------------------------+-----------+--------------+------------------------
1000 | Hydraulique Lyon 1000 | Lyon | 500 | 2026-01-01 00:00:00+00
1001 | Roulements Bruxelles 1001 | Bruxelles | 637 | 2026-01-01 00:01:00+00
1002 | Fixations Milano 1002 | Milano | 774 | 2026-01-01 00:02:00+00
The same partners as the REST quickstart, in the same
storage layout. XML carries no types: credit_limit and
updated_at are typed by their promote entries.
The manifest
| Field | Where | Required | Notes |
|---|---|---|---|
kind: soap |
endpoint | yes | Goes with pagination.kind: soap |
path |
endpoint | yes | The POST target, appended to base_url (e.g. /eai_enu/start.swe) |
body |
endpoint | yes | The inner <soap:Body> XML — the hub wraps the SOAP 1.1 envelope |
soap_action |
endpoint | no | Sent as the SOAPAction header (an empty quoted string when absent) |
header |
endpoint | no | The inner <soap:Header> XML — same substitution as body |
pagination.kind: soap |
endpoint | yes | Goes with kind: soap |
page_size |
pagination | yes | Rows per page, injected as {{page_size}} — explicit, no default |
response_path |
pagination | yes | A plain dotted path to the repeated element |
last_page_path / has_more_path |
pagination | no | One stop signal — see Where the QueryPage loop stops |
target |
endpoint | yes | The same hybrid target as REST: table, unique_key, promote |
- The request is always a
POSTwithContent-Type: text/xml; charset=utf-8. - SOAP 1.1 envelopes only. A service that requires SOAP 1.2 (
application/soap+xml) is not supported. - Set
page_sizeto the number of rows the service actually returns per page.
How it reads
- Write every path without namespace prefixes:
Customer, notns1:Customer. response_pathis a plain dotted path to the repeated element. A page holding a single element is still read.- A body with neither
{{cursor}}nor{{page}}is sent once: there is no next page to ask for.
Nothing is written until the last page is read
The pages are held in the hub's memory until the end. Size page_size × the number of pages
accordingly. A cancelled run writes nothing.
Body templating
Five placeholders are resolved on every request — in body: and header: alike:
| Placeholder | Resolves to |
|---|---|
{{page_size}} |
pagination.page_size, verbatim |
{{cursor}} |
the row offset of the page being fetched |
{{page}} |
the page number of the page being fetched, from 1 |
{{secret:NAME}} |
the lm secret registry, then the hub's process environment — see below |
{{env:NAME}} |
the hub's process environment only |
- An unresolved placeholder fails the run before any request is sent. The error names the placeholder, never a value.
- Any other placeholder (
{{watermark}}, a typo) is refused atlm validateandlm apply. The refusal names the placeholder and the key (bodyorheader).
A credential inside the envelope
{{secret:NAME}} reads the lm secret registry first, then the hub's environment, like
password_env. {{env:NAME}} reads the hub's environment only.
A session token inside the envelope goes in header::
# 94-soap-session.yaml — the credential that does not travel in a header.
#
# Legacy EAI services often want their session token INSIDE the envelope, in <soap:Header>,
# where no auth provider can put it. That is what the endpoint's own `header:` template is for —
# same substitution as `body:`, same placeholders, same fail-fast.
#
# {{secret:NAME}} reads the `lm secret` registry first and the hub's environment second, like
# password_env below. ({{env:NAME}} would read the hub's environment only.) So:
#
# lm secret set ACME_SESSION_TOKEN # demo-session-42
#
# An unresolved placeholder FAILS THE RUN before the request is sent — the template never goes
# on the wire with a literal {{...}} inside it, and the value is never echoed in the error.
#
# Expect (secret set): 2 400 rows.
# Expect (secret absent): a run that fails fast naming the placeholder, NOT a 401 — the refusal
# happens in the hub, before the service is ever asked.
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: acme-soap-session
connector_type: rest-generic
scopes: [billing]
spec:
base_url: http://host.docker.internal:8099
# Transport auth is still HTTP Basic — the session token is a SECOND credential, carried in
# the envelope. Both are needed.
auth:
kind: basic
username: demo-user
password_env: ACME_BASIC_PW
endpoints:
- id: partners
kind: soap
path: /session/start.swe
soap_action: "document/urn:acme:CustomerQueryPage"
header: |
<SessionToken>{{secret:ACME_SESSION_TOKEN}}</SessionToken>
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 500
last_page_path: "$.Body.CustomerQueryPage_Output.LastPage"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_session
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
Without the secret, nothing is sent:
$ lm source rest test acme-soap-session
ENDPOINT METHOD STATUS ITEMS VERDICT
partners POST - 0 unresolved secret/env placeholder: ACME_SESSION_TOKEN
Error: 1 of 1 enabled endpoint(s) did not answer with readable data
STATUS -: no request was sent.
Store it, and the next run sends it; the manifest does not change:
lm secret set ACME_SESSION_TOKEN # demo-session-42
Where the QueryPage loop stops
QueryPage has no total and no next link. The loop follows one signal: the first of these you set.
| Set | The loop continues |
|---|---|
last_page_path |
only while the flag reads explicitly false |
else has_more_path |
only while the flag reads explicitly true |
| neither | until a page comes back shorter than page_size |
A missing, null or unreadable flag stops the loop. "true"/"false" strings are read as
booleans.
The quickstart uses last_page_path; here are the other two, plus a single-element last page:
# 91-soap-stop-signals.yaml — the three ways a QueryPage loop ends, side by side.
#
# A SOAP run stops on exactly one signal, chosen by what you declare:
#
# last_page_path set → continue only while the flag reads "false"
# else has_more_path → continue only while the flag reads "true"
# else → stop on the first page shorter than page_size
#
# A missing or unreadable flag stops the run.
#
# The `tail` endpoint reads 201 rows, 100 per page: its last page holds ONE row. All 201 must
# land — a last page of one row is still read.
#
# Expect: 2 400 (more-records), 2 400 (short-page), 201 (single-element-tail).
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: acme-soap-stops
connector_type: rest-generic
scopes: [billing]
spec:
base_url: http://host.docker.internal:8099
auth:
kind: basic
username: demo-user
password_env: ACME_BASIC_PW
endpoints:
# ── has_more_path — the SAP PI flavour: "there are more", not "this is the last" ────────
- id: more-records
kind: soap
path: /sap/start.swe
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 500
has_more_path: "$.Body.CustomerQueryPage_Output.MoreRecords"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_moreflag
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
# ── no flag at all — the service announces nothing, the short page is the only signal ───
- id: short-page
kind: soap
path: /mute/start.swe
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 700 # 2 400 = 3 x 700 + 300 — the fourth page is the short one
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_shortpage
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
# ── the single-element last page — all 201 rows must land ────────────────
- id: single-element-tail
kind: soap
path: /tail/start.swe
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 100 # 201 rows = 100 + 100 + ONE
last_page_path: "$.Body.CustomerQueryPage_Output.LastPage"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_tail
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
$ lm run list --connector acme-soap-stops
ID CONNECTOR ENDPOINT STATUS IN OUT SKIPPED DEAD-LETTERS STARTED ENDED
4ad8f592 acme-soap-stops single-element-tail Completed 201 201 0 0 2026-10-19 01:46 2026-10-19 01:46
9ace6ce2 acme-soap-stops short-page Completed 2400 2400 0 0 2026-10-19 01:46 2026-10-19 01:46
993bbd1a acme-soap-stops more-records Completed 2400 2400 0 0 2026-10-19 01:46 2026-10-19 01:46
201, not 200: /tail holds 201 rows at a page size of 100, so its last page carries one
<Customer>, and it is ingested. On a new SOAP source, compare the row count with the source's.
When the service misbehaves
A Fault fails the run — and is never retried
A <soap:Fault> fails the run, whether it arrives on HTTP 500 (SOAP 1.1) or on HTTP 200 (some
gateways):
# 92-soap-fault.yaml — the run that must fail.
#
# A SOAP service says "no" with a <soap:Fault>, sent with HTTP 500 or with HTTP 200. Both fail
# the run, naming the faultstring. A Fault is the service's answer: it is never retried.
# Compare 93, the same status with a retry.
#
# Expect: BOTH endpoints Failed, 0 rows written, the run message naming the faultstring.
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: acme-soap-fault
connector_type: rest-generic
scopes: [billing]
spec:
base_url: http://host.docker.internal:8099
auth:
kind: basic
username: demo-user
password_env: ACME_BASIC_PW
endpoints:
- id: fault-on-500
kind: soap
path: /fault/start.swe
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 100
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_fault500
unique_key: "$.Id"
promote: []
- id: fault-on-200
kind: soap
path: /fault200/start.swe
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 100
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_fault200
unique_key: "$.Id"
promote: []
$ lm run list --connector acme-soap-fault
ID CONNECTOR ENDPOINT STATUS IN OUT SKIPPED DEAD-LETTERS STARTED ENDED
0de9fa56 acme-soap-fault fault-on-200 Failed 0 0 0 0 2026-10-19 01:46 2026-10-19 01:46
8d179cc0 acme-soap-fault fault-on-500 Failed 0 0 0 0 2026-10-19 01:46 2026-10-19 01:46
$ lm run get 8d179cc0
ID CONNECTOR ENDPOINT STATUS IN OUT SKIPPED DEAD-LETTERS STARTED ENDED
8d179cc0 acme-soap-fault fault-on-500 Failed 0 0 0 0 2026-10-19 01:46 2026-10-19 01:46
✖ run failed: SOAP Fault: SBL-DAT-00500: no such Business Component 'Customer'
- The message carries the service's
faultstringverbatim. - Nothing is ingested or dead-lettered. Neither endpoint's
ext.*table is created. - A Fault is never retried: the demo API logs one request per endpoint.
lm source rest testshows the same: itsVERDICTreadsSOAP Fault: …. A 500 without a Fault (an HTML page) readsHTTP 500.
The same 500, without a Fault, is retried
# 93-soap-retry.yaml — the other error channel, on the same status code.
#
# The /flaky service answers 500 twice per page window and then serves the page. The body of
# those 500s is XML, but it carries NO <soap:Fault> — it is an infrastructure hiccup, not an
# answer. The shared REST retry policy applies unchanged: 5 attempts, transient on exactly
# 429, 500, 502, 503, 504, 408.
#
# Read this file against 92. Same server, same 500, opposite outcome — and the body is the only
# thing that differs. That is the whole design: the status says "something went wrong", the
# body says whose fault it is.
#
# ── Before running ────────────────────────────────────────────────────────────────────────
# curl http://127.0.0.1:8099/admin/reset
#
# The failure counters are per page window and they persist. Without a reset, a second run finds
# the failures already burnt in, retries nothing, and goes green for the wrong reason.
#
# Expect: Completed with 2 400 rows, and visibly slower than 91's short-page endpoint over the
# same data — three requests per window instead of one, plus the backoff between them.
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: acme-soap-retry
connector_type: rest-generic
scopes: [billing]
spec:
base_url: http://host.docker.internal:8099
auth:
kind: basic
username: demo-user
password_env: ACME_BASIC_PW
endpoints:
- id: flaky
kind: soap
path: /flaky/start.swe
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 800 # 3 windows x 3 requests — enough retries to see, few to wait for
last_page_path: "$.Body.CustomerQueryPage_Output.LastPage"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_flaky
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
The /flaky service answers 500 without a Fault twice per page before serving it. The
REST retry policy applies unchanged.
$ lm run list --connector acme-soap-retry
ID CONNECTOR ENDPOINT STATUS IN OUT SKIPPED DEAD-LETTERS STARTED ENDED
520b32f6 acme-soap-retry flaky Completed 2400 2400 0 0 2026-10-19 01:46 2026-10-19 01:46
The demo API's log shows three requests per page, with a growing pause between them:
19/Oct/2026 01:46:32 "POST /flaky/start.swe HTTP/1.1" 500 -
19/Oct/2026 01:46:33 "POST /flaky/start.swe HTTP/1.1" 500 -
19/Oct/2026 01:46:34 "POST /flaky/start.swe HTTP/1.1" 200 -
19/Oct/2026 01:46:34 "POST /flaky/start.swe HTTP/1.1" 500 -
19/Oct/2026 01:46:35 "POST /flaky/start.swe HTTP/1.1" 500 -
19/Oct/2026 01:46:37 "POST /flaky/start.swe HTTP/1.1" 200 -
Same status code as the Fault above, opposite handling: a 500 with a Fault fails at once, a 500 without one is retried.
Reset the counters before re-running
curl http://127.0.0.1:8099/admin/reset. Otherwise a second run finds the counters spent
and retries nothing.
A refusal that reads I/O error, tried five times
A CoreWCF service on Kestrel can lose its Fault over HTTP/2. The run then fails without the Fault. It is retried like a network failure, 5 attempts in all:
$ lm source rest test scenario-wcf-probe
ENDPOINT METHOD STATUS ITEMS VERDICT
open-orders POST - 0 I/O error (IOException)
Ask for HTTP/1.1 in the connector. The same answer then arrives whole:
spec:
base_url: https://wcf-orders:18443
http_version: "1.1" # "1.1" or "2"; absent, the hub negotiates
- The Fault is read, the run fails with its
faultstring, and it is not retried. - Quote the value.
http_version: 1.1is a number to YAML, andlm validaterefuses it. - It is one setting for the connector, not for one endpoint.
- Successful calls do not show the problem: it appears on the first refusal, a rotated password for example.
What lm apply refuses
kind: soapwithoutpagination.kind: soap, or the reverse.- A
response_pathwith[*]or..: write a plain dotted path. - A missing
page_size.
Each refusal names the key at fault, e.g. spec.endpoints[0].pagination.page_size.
Incremental — the mark rides the URL, not the body
incremental: { mode: watermark } works on a SOAP endpoint:
- The stored mark goes out on every request of the run, under the query parameter
paramnames. - The new mark is the highest
watermark_pathover the rows read. - As on REST, the run asks from a little before the mark: a change during a run.
# 95-soap-watermark.yaml — incremental over SOAP.
#
# `incremental: watermark` sends the last mark on the first request of a run, under the
# parameter named by `incremental.param`. On SOAP, that parameter goes in the URL's query
# string, not in the body:
#
# POST /siebel/start.swe?updated_since=2026-01-02T15:59:00Z
#
# The body template has no {{watermark}} placeholder: `lm apply` refuses one. A service that
# wants the filter inside the body cannot be read incrementally today.
#
# lm connector run acme-soap-incremental # run 1 — the full 2 400
# lm connector run acme-soap-incremental # run 2 — only what changed
# lm endpoint reset-cursor acme-soap-incremental/partners
#
# Expect: run 1 = 2 400 in / 2 400 out. Run 2 = only the rows changed since run 1 (a little
# before, so nothing changed during run 1 is missed). They replace their earlier version: the
# table stays at 2 400.
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: acme-soap-incremental
connector_type: rest-generic
scopes: [billing]
spec:
base_url: http://host.docker.internal:8099
auth:
kind: basic
username: demo-user
password_env: ACME_BASIC_PW
endpoints:
- id: partners
kind: soap
path: /siebel/start.swe
soap_action: "document/urn:acme:CustomerQueryPage"
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
</CustomerQueryPage_Input>
incremental:
mode: watermark
param: updated_since # a query parameter on the POST — not a body element
watermark_path: "$.UpdatedAt"
pagination:
kind: soap
page_size: 500
last_page_path: "$.Body.CustomerQueryPage_Output.LastPage"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.acme_soap_incremental
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
- { name: updated_at, path: "$.UpdatedAt", type: timestamptz, transform: parse_iso8601 }
Run 1 reads the 2 400 rows. Run 2 reads only the rows changed since the mark, and upserts them on their key: the table stays at 2 400 rows.
Know this before you promise a customer an incremental SOAP feed
The mark goes in the URL query string, not in the body: there is no {{watermark}}
placeholder. The request reads POST /siebel/start.swe?updated_since=….
A service that expects the filter inside the body ignores it. Then filter in the body with a
literal and leave incremental at full.
Connector-level headers
spec.headers[] applies to every SOAP request, as for REST. Do not
redeclare Content-Type or SOAPAction: the engine sets them.
How to…
Point it at your own service
The TUI's :new picker hands you this manifest for soap — a Siebel EAI endpoint:
# Siebel CustomerQueryPage over SOAP, exposed as a canonical hub table.
# kind: soap is a request-shape on the HTTP engine (connector_type stays rest-generic).
apiVersion: connectors.lumnik.io/v1
kind: Connector
metadata:
name: siebel-customers
connector_type: rest-generic
scopes: [crm, customers, siebel]
spec:
base_url: https://siebel.example.com
auth:
kind: basic
username: siebel_svc # literal username (the basic block reads `username`, not an env)
password_env: SIEBEL_PWD # env var name holding the password
endpoints:
- id: customers
kind: soap
path: /eai_enu/start.swe
soap_action: "document/urn:crmod:...:CustomerQueryPage"
body: |
<CustomerQueryPage_Input>
<PageSize>{{page_size}}</PageSize>
<StartRowNum>{{cursor}}</StartRowNum>
<ListOfCustomer><Customer><Id query="*"/><Name query="*"/></Customer></ListOfCustomer>
</CustomerQueryPage_Input>
pagination:
kind: soap
page_size: 100
last_page_path: "$.Body.CustomerQueryPage_Output.LastPage"
response_path: "$.Body.CustomerQueryPage_Output.ListOfCustomer.Customer"
target:
kind: hybrid
table: ext.siebel_customers
unique_key: "$.Id"
promote:
- { name: name, path: "$.Name", type: text }
Change base_url, the path your EAI listener publishes, the operation names inside body:
and the response_path. Then:
lm apply -f siebel-customers.yaml
lm source rest test siebel-customers
Read a custom service that pages by number
Custom business services often take a page number in the body and say on the page whether it is
the last one, in an attribute. Put {{page}} where the number goes; an attribute reads like an
element:
body: |
<cus:getListOpportunity_Input xmlns:cus="http://siebel.com/CustomUI">
<getOpportunity>
<Opportunity>
<refExtPersonne>40012345</refExtPersonne>
<page>{{page}}</page>
</Opportunity>
</getOpportunity>
</cus:getListOpportunity_Input>
pagination:
kind: soap
page_size: 3 # the service's own page size: no placeholder sends it
last_page_path: "$.Body.getListOpportunity_Output.getOpportunity.lastpage"
response_path: "$.Body.getListOpportunity_Output.getOpportunity.Opportunity"
The pages asked are 1, 2, 3… until lastpage reads true.
Read a service that does not page
A service that answers everything in one call (the actions of one case, say) needs no paging
placeholder. Leave {{cursor}} and {{page}} out of the body and header: the endpoint sends one
request per run, whatever the answer holds. pagination.kind: soap, page_size and
response_path are still required.
Read a second collection from the same service
Add a second endpoint with its own body:, response_path and table. The ledger records one
run per endpoint, as the stop-signals connector shows.
Troubleshooting
| Symptom | Cause | Gesture |
|---|---|---|
Run fails instantly with unresolved secret/env placeholder |
secret: — the name is neither in lm secret nor in the hub's environment; env: — it is not in the hub's environment |
lm secret set NAME for secret:; the hub's environment for env: |
| Run fails on HTTP 500 with no retry | a <soap:Fault> is never retried |
read the faultstring in lm run get; fix the request or the permission it needs |
I/O error (IOException) on a service that should answer a Fault, five POSTs per run in its log |
the service resets its HTTP/2 stream after the answer | set http_version: "1.1" in the connector |
Run failed on pagination.response_path reads nothing on the first page |
response_path does not match the answer |
write the path without namespace prefixes; lm source rest test shows the same message as its verdict |
| A run reads one row fewer than the source holds | an intermediate segment of response_path repeats in the answer |
the last page holding a single element is where the loss shows; compare the row count with the source's |
| Pagination stops after one page | the flag is missing, null or unreadable, which stops the loop | check last_page_path / has_more_path against the real answer, or set neither and rely on the short page |
See also
- REST — the engine underneath (auth providers, retry, rate limit, storage layout)
- GraphQL — the other request shape on that engine
- The demo API — the services these screens read
- Overview
- The WOW walkthrough ingests a fake Siebel over this connector — see it end to end
- Diagnosing a failed run