APE Protocol Deep Dive
This guide covers CDS entity extraction in ERPL through SAP's ABAP Pipeline Engine. By the end you will know how to:
- ✅ Find and describe the CDS entities a system exposes
- ✅ Read one in full, or as a delta that includes deletes
- ✅ Manage the subscriptions a delta read creates, and drop them cleanly
- ✅ Recover an interrupted delta read without losing committed changes
- ✅ Check, before you start, that the SAP user has what it needs
The ABAP Pipeline Engine (DHAPE_*) is a data-flow engine that ships inside the ABAP stack. It
runs a small graph of operators — a reader, a channel, an outport — and hands the result back
over RFC in packages. ERPL drives it with one operator: the CDS reader. Together with the ABAP
Metadata Browser (DHAMB_*) for discovery, that is the whole surface. Nothing is installed in
SAP — no transport, no Z objects, no ABAP.
Why a second path to CDS data
ERPL already reads ABAP CDS through ODP, and for many entities that remains the right choice. Two things push the other way.
SAP Note 3255746 restricts third-party use of the ODP RFC API. erpl_ape exists because the
pipeline engine is a different, supported interface for the same data. It reads CDS entities and
nothing else — and that restriction is not a promise, it is enforced twice over: by a
compiled-in allow-list of pipeline operators that no setting widens, and by the SAP role, whose
S_DHAPEOPR object is checked per operator name. A role built as we document it cannot drive
an ODP or SLT operator even if the software tried to. See the
ZERPL_APE role.
Delta through ODP needs an annotation the entity may not have. ODP delta on a CDS view
requires @Analytics.dataExtraction.delta.byElement — a design-time decision taken by whoever
built the view, usually a change-timestamp element. That gives you inserts and updates. It does
not give you deletes, because a deleted row has no timestamp to report. The pipeline engine
uses trigger-based change capture (DHCDC) instead, so a delete arrives as a row marked D.
ODP (ABAP_CDS context) | APE (pipeline engine) | |
|---|---|---|
| Interface | RODPS_REPL_* | DHAPE_* / DHAMB_* |
| Delta prerequisite | byElement annotation on the view | none; triggers are generated on first delta |
| Deletes in delta | no | yes, as /1DH/OPERATION = 'D' |
| Server-side filters | yes, as ABAP range selections | no — filter in SQL instead |
| Parallel package fetch | yes, threads => | no, one package at a time |
| SAP footprint | delta queue entries | DHCDC logging tables and triggers, until the subscription is dropped |
Neither replaces the other. ODP remains the better fit when the entity is already annotated, when you need server-side selections, or when you want parallel fetching. Reach for APE when you need deletes, or when the ODP RFC path is not available to you.
APE Architecture
Like BICS and ODP, APE runs entirely over the SAP
NetWeaver RFC SDK and reuses the RFC extension's sap_rfc secret. There is
no separate endpoint to configure.
One structural point worth knowing early: DHAPE_GRAPH_ROUNDTRIP is the only call that moves
data, and the engine commits each package as it hands it over. It cannot re-send one. That is
why ERPL keeps a local copy in erpl_ape.delta_spill — see Recovering an interrupted
read.
Connecting
APE uses the same secret as every other ERPL protocol:
CREATE SECRET sap (
TYPE sap_rfc,
ASHOST 'sap.example.com',
SYSNR '00',
CLIENT '100',
USER 'ERPL_APE',
PASSWD 'secret',
LANG 'EN'
);
LOAD erpl loads erpl_ape alongside erpl_rfc, erpl_bics and erpl_odp, so there is
nothing extra to install.
Before the first extraction, check that the user can actually do the job:
SELECT auth_object, checked_values, verdict, note
FROM sap_ape_check_authorizations('I_SALESORDER')
WHERE verdict <> 'ok';
An empty result means the role is complete. Anything else names what is missing, or — for one row — what is granted and should not be. See the role page.
Discovering entities
sap_ape_show([search, released_only, max_depth, secret])
Browses the metadata browser's CDS tree. With no arguments it walks from the root; search
narrows it, which is much faster on a large system.
-- Find entities whose name contains SALESORDER
SELECT object_name, object_path, released
FROM sap_ape_show(search => 'SALESORDER');
-- Only entities with a release contract, to a bounded depth
SELECT * FROM sap_ape_show(released_only => true, max_depth => 4);
released matters: an entity without a release contract may change shape without notice, and
ERPL refuses to extract from one unless you set erpl_ape_allow_unreleased = true.
sap_ape_describe(cds_name [, secret])
Field structure — ABAP type, length, decimals, and the DuckDB type ERPL will produce.
SELECT field_name, abap_type, length, decimals, duckdb_type
FROM sap_ape_describe('I_SALESORDER');
The argument is an entity name or its browser path; a bare name is resolved by browsing.
sap_ape_preview(cds_name [, max_rows, secret])
A bounded sample, read directly from the metadata browser rather than through the pipeline. That is the point: it needs no graph and no subscription, so it is the cheap way to look at an entity before committing to an extraction.
SELECT * FROM sap_ape_preview('I_SALESORDER', max_rows => 20);
Full extraction
sap_ape_read_full(cds_name [, columns, filters, chunk_size, wireformat, recover, secret])
-- Everything
SELECT * FROM sap_ape_read_full('I_SALESORDER');
-- A projection, pushed into the graph
SELECT * FROM sap_ape_read_full('I_SALESORDER',
columns => ['SalesOrder', 'SoldToParty', 'TotalNetAmount']);
-- Smaller packages: more roundtrips, less memory per package
SELECT count(*) FROM sap_ape_read_full('I_SALESORDER', chunk_size => 10000);
A full read creates a subscription for the duration of the scan and erases it when the scan
ends — including when you abandon it early with a LIMIT.
filters is refused, not ignoredThe pipeline's CDS reader on the current release reads exactly seven configuration keys, and a
filter is not among them. Passing filters therefore raises an error rather than being
silently dropped: emitting a filter the engine does not read would return every row while your
query looked filtered, which is worse than failing. Filter with an ordinary SQL WHERE — the
scan streams, so DuckDB applies predicates as rows arrive.
Delta extraction
sap_ape_read_delta(cds_name, subscriber_process [, columns, filters, chunk_size, wireformat, recover, secret])
subscriber_process names the consumer. It is how SAP recognises the subscription on the next
call, so the same value resumes where the last read stopped.
-- First call: initial load, and the subscription is created
SELECT count(*) FROM sap_ape_read_delta('I_SALESORDER', 'NIGHTLY_ETL');
-- Later calls: only what changed since
SELECT * FROM sap_ape_read_delta('I_SALESORDER', 'NIGHTLY_ETL');
Reading the change indicator
Delta rows carry an extra column, /1DH/OPERATION:
| Value | Means |
|---|---|
U | The row's current state — an insert or an update, both delivered as after-images |
D | The row was deleted. Only the key fields are populated |
Inserts and updates are not distinguished, because the engine sends after-images for both. That is the engine's semantics, not an ERPL simplification.
-- Apply a delta to a local table
BEGIN;
CREATE TEMP TABLE chg AS
SELECT * FROM sap_ape_read_delta('I_SALESORDER', 'NIGHTLY_ETL');
DELETE FROM salesorder
WHERE SalesOrder IN (SELECT SalesOrder FROM chg WHERE "/1DH/OPERATION" = 'D');
DELETE FROM salesorder
WHERE SalesOrder IN (SELECT SalesOrder FROM chg WHERE "/1DH/OPERATION" = 'U');
INSERT INTO salesorder
SELECT * EXCLUDE ("/1DH/OPERATION") FROM chg WHERE "/1DH/OPERATION" = 'U';
COMMIT;
A first delta on an entity makes SAP generate DHCDC logging tables and triggers on the
underlying tables, in a background job. While that job is pending the engine returns neither
data nor an error — the read simply waits. Batch work processes must be available or it appears
to hang. erpl_ape_prepare_timeout (default 900 s) bounds the wait; the application log to check
is object DHCDC, subobject ACP_JOB.
Subscriptions
A delta subscription is durable server-side state: the triggers and logging tables persist until it is dropped. Treat it as something you own and clean up.
sap_ape_show_subscriptions([erpl_only, cds_name, secret])
SELECT cds_name, subscriber_process, subscription_name, transfer_mode, status, created_at
FROM sap_ape_show_subscriptions();
-- Including subscriptions ERPL did not create
SELECT * FROM sap_ape_show_subscriptions(erpl_only => false);
Two name columns, and the difference matters. ERPL registers subscriptions with an ERPL_
prefix so the engine's inventory says which are ours: subscriber_process is the value you
passed and the one that round-trips into sap_ape_drop, while subscription_name is what SAP
stores and what a Basis administrator sees in DHCDC_MON.
PRAGMA sap_ape_drop(cds_name, subscriber_process)
PRAGMA sap_ape_drop('I_SALESORDER', 'NIGHTLY_ETL');
Pass the unprefixed name — the one you used when reading. It reports rather than raises
('DROPPED' or 'NOT_FOUND'), because cleanup in SQL has no try/catch and a pipeline needs to
be able to tidy up best-effort.
If a process dies while its graph is running, the graph stays running server-side and the engine then refuses to erase the subscription it holds. Recovery is the engine's own retention mechanism; there is nothing to do from the client but wait for it.
Recovering an interrupted read
SAP commits each delta package as it hands it over, and cannot re-send it. So if your process dies mid-scan, those changes are gone as far as SAP is concerned — the pointer has moved. This is the one place APE needs help from the client.
ERPL writes every handed-over package to a local table, erpl_ape.delta_spill, before its rows
are consumed, on its own transaction so a rollback in your transaction cannot discard it.
-- After a crash: replay what was handed over but never consumed
SELECT * FROM sap_ape_read_delta('I_SALESORDER', 'NIGHTLY_ETL', recover => true);
recover => true touches no SAP state at all — it reads the spill and nothing else, which
means it works with SAP unreachable, and it is repeatable. It replays whole packages, so you may
see rows you already consumed; the operation is at-least-once, not exactly-once.
erpl_ape.delta_spill contains complete, unredacted rows for as long as the batch is the newest
one, and anyone who can open the database file can read it. On a persistent database the last
batch lives until the next delta read on that subscriber replaces it. Set
erpl_ape_spill_enabled = false to opt out — at the cost of making delta extraction
at-most-once.
Performance and good practice
Use chunk_size to trade roundtrips against memory. It is rows per package. Smaller
packages mean more calls and a smaller working set; the default suits most entities. Unlike ODP,
APE fetches one package at a time — there is no threads parameter, because the engine hands
packages over on a single session.
Project early. columns => [...] is pushed into the reader, so unwanted columns never cross
the wire. A SQL-level projection does not help the transfer.
Prefer sap_ape_preview for exploration. It needs no graph and creates no subscription, so
it costs the SAP system almost nothing compared with a full read you abandon.
Delta beats repeated full reads, but not for free. Weigh the ongoing cost of triggers and logging tables on the source tables against re-reading the entity. For a small, rarely changing entity a periodic full read is often the better engineering choice.
Don't leave subscriptions lying around. Every one you no longer read is trigger overhead on
a production table. sap_ape_show_subscriptions() then sap_ape_drop is a good habit.
Troubleshooting
Common Issues
Entity not found
-- Search rather than guess; names are case-sensitive at the API
SELECT object_name, object_path FROM sap_ape_show(search => 'SALESORDER');
The read seems to hang
-- Usually SAP's preparation job. Bound the wait, then look in SAP.
SET erpl_ape_prepare_timeout = 1800;
Check that batch work processes are free, and read application log object DHCDC, subobject
ACP_JOB. A first delta is much slower than a full load by design.
"already has a subscription" on the second call
-- The subscriber already exists. Resume it by using the same name...
SELECT * FROM sap_ape_read_delta('I_SALESORDER', 'NIGHTLY_ETL');
-- ...or discard it and accept a fresh initial load
PRAGMA sap_ape_drop('I_SALESORDER', 'NIGHTLY_ETL');
Permission denied, or nothing works at all
-- The whole requirement list, with a verdict per row
SELECT * FROM sap_ape_check_authorizations('I_SALESORDER');
If every row reports unknown, the connection or AUTHORITY_CHECK itself could not be
reached — usually a missing S_RFC grant for function group SUSR.
An unreleased entity is refused
-- Deliberate: such an entity may change shape without notice
SET erpl_ape_allow_unreleased = true; -- logged at WARN
Debugging Tips
SET erpl_trace_enabled = true;
SET erpl_trace_level = 'DEBUG';
SET erpl_trace_output = 'console';
PRAGMA sap_ape_ping; -- logs on AND asks the engine its version
SELECT * FROM sap_ape_system_info();
PRAGMA sap_ape_ping is deliberately more than a logon check: it also calls the engine, so it
fails when the DHAPE_* function group is unreachable for this user rather than only when
credentials are wrong.
For SAP Experts
The wire-level mechanics for Basis administrators and ABAP developers — the DHAPE_* and
DHAMB_* modules, the graph, the package format, and the change-capture semantics behind the
SQL functions.
APE protocol deep dive — DHAPE/DHAMB modules, the graph, package format, DHCDC semantics, authorization call sites
The function modules
| ERPL function | RFC function module(s) |
|---|---|
sap_ape_show | DHAMB_SERVICE_DSET_BROWSE (once per folder level) |
sap_ape_describe | DHAMB_SERVICE_DSET_BROWSE (path resolution) → DHAMB_SERVICE_DSET_DEFINITION |
sap_ape_preview | DHAMB_SERVICE_DSET_PREVIEW |
sap_ape_read_full / read_delta | DHAPE_GRAPH_MANAGER → DHAPE_GRAPH_ROUNDTRIP (repeatedly) → DHAPE_GRAPH_MANAGER |
sap_ape_show_subscriptions, PRAGMA sap_ape_drop | the same two, driving an admin graph |
PRAGMA sap_ape_ping | SDK RfcPing() + DHAPE_GRAPH_VERSION |
sap_ape_system_info | DHAMB_SERVICE_SYSTEM |
sap_ape_check_authorizations | AUTHORITY_CHECK |
The DHAMB_SERVICE_* modules are REST endpoints tunnelled over RFC: they take a path and return
a JSON body with an HTTP status. The DHAPE_* ones are the engine proper.
The graph
DHAPE_GRAPH_MANAGER takes a mode — Create, Run, Stop, or Open — and, for Create, a graph
definition as JSON: a set of named processes, each with a Component naming its operator, and
channels connecting their ports. Protocol v6 auto-starts the graph during Create, and the
graph is bound to the RFC session.
ERPL builds one shape: a CDS reader (com.sap.abap.cds.reader.v2) feeding an outport
(com.sap.abap.internal.outport, which the engine synthesises). The reader takes exactly seven
configuration keys — subscriptionType, subscriptionID, subscriptionName, cdsname,
action, chunkSize, wireformat. There is no filter key and no schema key, which is why
filters is refused rather than emitted.
action selects the mode: Initial Load for a full read, Replication for delta. A third value,
Delta Load, exists in the engine and is deliberately not exposed — it maps to a DHCDC process
option that never prepares an initial load, so it would stall permanently.
The package format
DHAPE_GRAPH_ROUNDTRIP returns packages as a JSON envelope with Encoding: "csv" and a
self-describing Attributes.ABAP.Fields[] block, so each package carries its own schema. The
body is RFC 4180 quoted — "comma,inside", "quote""inside" — and a backslash is data, not
an escape. message.batchIndex counts packages and message.lastBatch marks the end.
A replication graph never sets lastBatch; a delta read ends when no further package arrives
within erpl_ape_delta_quiet_seconds. Documented sentinel values (9999-99-99, NaN, ? and
similar) map to NULL; any other value that will not cast raises an error naming the package, row,
column and text, because a silent NULL is a wrong answer the caller cannot see.
Change capture
Delta uses DHCDC: on first use SAP generates logging tables and database triggers on the tables
underlying the entity, in a background job (DHCDC / ACP_JOB). They persist until the
subscription is erased. The change column is /1DH/OPERATION, with U for insert and update
after-images and D for a delete carrying only keys.
The engine commits each package on handover — ERPL calls commit_delta_data immediately after
output_data, and there is no client acknowledgement in the protocol — so there is no
engine-side recovery of an unconfirmed package. That is what the local spill is for.
Where authorizations are actually checked
| Object | Field(s) | Checked by |
|---|---|---|
S_DHAPEOPR | DHAPEOPNM, ACTVT | CL_DHAPE_AUTHORITY::check_operator, from CL_DHAPE_OPERATOR_REGISTRY, once per operator the graph names |
S_DHAPEOP | DHAPEOPIMP | the same method, as a fallback when the S_DHAPEOPR check fails |
S_DHAPEAC2 | ACTVT | CL_DHAPE_AUTH_ACTIVITY |
S_DHAMBACT | DHAMB_ACVT | CL_DHAMB_AUTHORITY::check_activity |
S_DHAMBCDS | DHBASCDSNM, DHBASCDSRS, ACTVT | CL_DHAMB_AUTH_CDS |
Two consequences worth the attention of whoever builds the role. S_DHAPEOPR being keyed on the
operator name is what makes the CDS-only restriction expressible in a role at all. And
S_DHAPEOP must not be granted: it is the fallback consulted when the S_DHAPEOPR check
fails, so holding it — from any role — authorises operators S_DHAPEOPR denies. The
role page covers both.
Next Steps
🚀 Ready for More?
- ODP Protocol Guide - The other path to CDS data, and when to prefer it
- RFC Protocol Guide - Read SAP tables and call functions
- Function Reference - Complete API docs
🔧 Advanced Topics
- APE Delta and Recovery - Subscriptions, the spill, and at-least-once delivery
- The ZERPL_APE Role - What to grant, and what deliberately not to
- Performance Tuning - Optimize extractions
💡 Examples
- ERPL Examples - More real-world examples
- Integration with Python - Use extracted data with Pandas
- Real-World Use Cases - Complete scenarios
Need help? Check our troubleshooting guide or browse more examples.