Migrate SEGW OData V2 service ➜ RAP service
Reverse-engineer a classic SEGW-built OData V2 service (MPC/DPC/MPC_EXT/DPC_EXT) into a modern RAP service (CDS root + projection + BDEF + SRVD + SRVB + behavior pool) on the same SAP system. Runs side-by-side: the legacy service stays live; the new RAP service lands in a separate, resettable package.
Domain example. Templates in this skill use an illustrative
Project → Tasks → TimeEntriesdomain (entity names likeZR_DM_PROJECT,ZBP_DM_PROJECT,ZDM_PROJECT_D) so the shape is concrete. Substitute the user's entities throughout — the LLM running this skill should rewrite every entity identifier to match the source service.
Smart defaults (apply silently — do NOT ask before research)
| Setting | Default | Rationale |
|---|---|---|
| Discovery strategy | MPC class source (Tier 1) | ARC-1 reads it natively; richer than $metadata |
| Target package | User-provided. If absent, create a child of the source package via SAPManage(action="create_package"); default name <source_package>_RAP. Only fall back to $TMP if the user explicitly asks. |
Keeps the migration output isolated and resettable. |
| Target transport | User-provided existing, or auto-created via SAPTransport(action="create", description="RAP migration of <legacy_service>", package="<target_package>"). |
Required for non-$TMP packages. |
| OData version on target | V4 | Current SAP standard; FE-ready |
| RAP scenario | Managed with internal numbering | Simplest read-mostly; matches legacy SEGW behaviour |
| Draft | Off for read-mostly entities. ON when the legacy service exposed CUD (function imports, deep inserts, or sap:updatable=true on entity sets) and the user wants Fiori Elements compatibility. | Match the legacy service's behavior contract; FE list+OP work best with draft on. |
| Strict mode | strict ( 2 ) |
Current best practice |
| Projection layer | Mandatory — always create a ZC_* projection alongside every ZR_* root view. Service binding exposes the projection, never the root. |
Run 1 of the skill skipped projections after a misread CDS error; spell out so the LLM never drops them. |
provider contract on projections |
On the root projection only, not on the root view, not on child projections. The contract sits at exactly one level. Use transactional_query when combining projection BDEF + use draft on 7.58; transactional_interface for read-mostly without draft. |
Putting it on a root view → "only valid on projection views". Putting it on child projections → "inappropriate provider contract on …". Children are exposed via redirected to parent and inherit the BO contract from the root projection. Run 3 confirmed _query is needed for draft on 7.58. |
| CDS composition syntax (managed) | composition [0..*] of <child> as <name> — no on clause. Key linking is implicit via matching key fields / with foreign key in the child |
7.58 rejects on on managed compositions. The recovery is dropping on, not switching to association to. |
@Semantics.* on 7.58 |
createdAt, lastChangedAt, and localInstanceLastChangedAt are all valid on 7.58 — put localInstanceLastChangedAt on the local-instance etag field (abp_locinst_lastchange_tstmpl), exactly as SAP's own RAP generator does. @Semantics.businessDate.from/to also work. |
Verified live on S/4HANA 2023 (758, 2026-06-12): standalone CDS views with localInstanceLastChangedAt and with businessDate.from/to both activate. An earlier "unknown annotation on 7.58" note was a misdiagnosis — draft annotations are genuinely absent only on pre-7.55 releases. |
| Draft table field names (when draft=ON) | Use BO-alias casing without underscores — e.g. projectid, startdate, NOT project_id, start_date. ABAP normalizes BDEF aliases (ProjectId, StartDate) to PROJECTID, STARTDATE — the draft table lookup is by that normalized name, not by the active table's snake_case. |
Run 2: BDEF activation failed with "key field PROJECTID expected at position 2, found PROJECT_ID". The active table can keep snake_case (BDEF mapping handles it); the draft table cannot — it has no mapping clause. |
| Naming | SAP-standard Z<prefix>_<entity>: ZR_ root, ZC_ projection, ZI_<entity>_BEH BDEF, ZBP_ behavior pool, ZUI_<service>_O4 V4 SRVB |
Aligns with SAP-internal conventions; the leading Z is the customer namespace |
| Pre-write lint | On | Set SAP_ABAP_RELEASE=<your_release> in ARC-1 config (PR #255) so the lint preset matches the system release. The older SAP_LINT_BEFORE_WRITE=false workaround is no longer needed. |
| Pre-write SAP check | On for activation-blockers only | We rely on activation feedback, not --check-before-write |
Input
The user must provide at minimum:
- One of: a SEGW service technical name (e.g.
Z<SOMETHING>_SRV), a package containing the SEGW-generated classes, or an MPC class name directly. Ask if not given. - Target package for the new RAP objects. Skill will create one (
<source_package>_RAP) if not given and the source package is known; otherwise asks. Only defaults to$TMPif the user explicitly asks. - Transport for non-
$TMPwrites — existing or auto-created (see Smart Defaults).
Optional:
- RAP scenario (default: managed with internal numbering).
- Draft on/off (skill infers from legacy CUD surface).
- OData version (default: V4).
- Naming overrides (default: SAP standard
Z<prefix>_<entity>).
If only the legacy identifier is given, skill applies smart defaults and surfaces the resolved
plan in Phase 5 for user ok before any writes.
Phase 0 — Authorization preflight
Run before any modelling work. Aborts cleanly if anything is missing, with the specific SAP auth object the user needs.
0a. Server-side capability + auth probe
SAPManage(action="probe")
Assert from the response:
systemType∈ {onprem,btp} — needed for naming + admin-field choicesrap.available == true— fail with: "This system doesn't expose RAP/CDS endpoints. Migration cannot proceed."transport.available == true— fail with: "Transport API unavailable; check ICF/sap/bc/adt/cts/transports."authProbe.searchAccess == true— fail with: "User cannot search ADT objects; needS_DEVELOPACTVT=03 OBJTYPE=CLAS DEVCLASS=<source-pkg>."authProbe.transportAccess == true— fail with: "User cannot read transports; needS_TRANSPRTACTVT=03."
0b. Read the legacy MPC class — confirms read on source
SAPRead(type="CLAS", name="<MPC>", method="*")
On HTTP 403: stop with "Cannot read <MPC> — need S_DEVELOP ACTVT=03 OBJTYPE=CLAS
DEVCLASS=<source-pkg> P_GROUP=<auth-group>. Run SU53 in SAP GUI to see the missing field."
0c. Confirm write on target package
SAPTransport(action="check", objectType="DDLS", objectName="ZX_PRECHECK", package="<target_package>")
Then dry-run a write+delete on a throwaway DDLS:
SAPWrite(action="create", type="DDLS", name="ZR_DM_PRECHECK",
source="@AccessControl.authorizationCheck: #NOT_REQUIRED\ndefine root view entity ZR_DM_PRECHECK as select from t000 { client }",
description="ARC-1 preflight",
package="<target_package>",
transport="<transport>")
SAPWrite(action="delete", type="DDLS", name="ZR_DM_PRECHECK")
On 403 from create: stop with "Cannot write to <target_package> — need S_DEVELOP ACTVT=01,02,07
OBJTYPE=DDLS DEVCLASS=<target_package>."
If all three pass, print "Phase 0 ✓ Authorizations OK — proceeding to discovery." and continue.
Phase 1 — Discover the legacy service
1a. Resolve the MPC class name
If user gave a service name (e.g. <legacy_service>):
- Try convention: trim
_SRV→ append_MPC→<MPC_class>. Verify withSAPRead. - If not found:
and ask the user to pick.SAPQuery(action="sql", sql="SELECT obj_name, devclass FROM tadir WHERE pgmid = 'R3TR' AND object = 'CLAS' AND obj_name LIKE 'Z%_MPC' ORDER BY obj_name")
If user gave a package (e.g. <source_package>):
Filter for classes endingSAPRead(type="DEVC", name="<package>")_MPC.
If user gave an MPC class directly: use it as-is.
Determine the corresponding _MPC_EXT, _DPC, _DPC_EXT by name convention.
1b. Confirm the four classes exist + record metadata
SAPSearch(action="object", query="<base>_*", maxResults=10)
Expected: 4 hits. Record objectName + packageName for each.
Print to user:
Legacy service: <legacy_service>
Package: <source_package>
MPC class: <MPC_class> (model)
MPC_EXT: <MPC_EXT_class> (model overrides — usually empty)
DPC class: <DPC_class> (data provider — generated)
DPC_EXT: <DPC_EXT_class> (data provider — your custom code)
Phase 2 — Extract the OData model from MPC source
The MPC class's private DEFINE_* methods carry the full model. Read them.
2a. Method inventory
SAPRead(type="CLAS", name="<MPC>", method="*")
You're looking for:
DEFINE_<entity>— one per entity typeDEFINE_ASSOCIATIONS— all associations + nav propertiesDEFINE_ACTIONS— all function imports
2b. Per-entity read
For each DEFINE_<entity> method:
SAPRead(type="CLAS", name="<MPC>", method="DEFINE_<ENTITY>")
Parse the body for:
- Entity type name + entity set name (look for
create_entity_type(...)andcreate_entity_set(...)) - Underlying ABAP DDIC bind (look for
bind_structure(...)orset_data_source(...)) - Each property: name, EDM type, max-length, is-key, sortable/filterable/creatable/updatable
(look for
create_property(...)followed byset_*calls)
2c. Associations
SAPRead(type="CLAS", name="<MPC>", method="DEFINE_ASSOCIATIONS")
Parse for each create_association(...):
- Association name
- Principal entity + cardinality
- Dependent entity + cardinality
- Navigation property name(s)
- Referential constraints (principal property ↔ dependent property)
2d. Function imports
SAPRead(type="CLAS", name="<MPC>", method="DEFINE_ACTIONS")
Parse for each create_action(...):
- Action / function-import name
- HTTP method
- Return type (entity / complex / primitive / void)
- Return cardinality
- Parameters: name, type, length, mode (in/out/inout)
2e. Compile into a structured model
After Phase 2, you should be able to print a complete table like:
=== Extracted OData model ===
Entity types (3):
Project bound to ZDM_PROJECT key: ProjectId 12 properties
Task bound to ZDM_TASK key: TaskId 15 properties (FK ProjectId)
TimeEntry bound to ZDM_TIMEENTRY key: EntryId 15 properties (FKs TaskId, ProjectId)
Entity sets (3): ProjectSet, TaskSet, TimeEntrySet
Associations (2):
Project_Tasks Project [1] ↔ Task [0..n] ref: ProjectId ↔ ProjectId nav on Project: Tasks
Task_TimeEntries Task [1] ↔ TimeEntry [0..n] ref: TaskId ↔ TaskId nav on Task: TimeEntries
Function imports (1):
ApproveProject POST in: ProjectId (Edm.String, len 10) return: Project (Entity, 1)
Print it and ask the user "Does this match what you expected?" before proceeding.
Phase 3 — Extract the behavior from DPC_EXT
3a. Method inventory
SAPRead(type="CLAS", name="<DPC_EXT>", method="*")
Categorize each redefined method:
| Method pattern | Maps to RAP |
|---|---|
<EntitySet>_GET_ENTITYSET |
CDS view's read access — usually free in managed scenario |
<EntitySet>_GET_ENTITY |
Same — free in managed scenario |
<EntitySet>_CREATE_ENTITY |
BDEF create enabled (with optional determination/validation) |
<EntitySet>_UPDATE_ENTITY |
BDEF update enabled |
<EntitySet>_DELETE_ENTITY |
BDEF delete enabled |
/IWBEP/IF_MGW_APPL_SRV_RUNTIME~EXECUTE_ACTION |
BDEF static or instance action(...) per function import |
3b. Per-method body read (only the ones with logic)
For each redefined method, decide:
- Trivial GET (just
SELECT * FROM <table> WHERE <key>+MOVE-CORRESPONDING): drop entirely — RAP managed scenario does this for free. - Filtered GET (additional WHERE clauses, derived fields): translate to CDS view filters or field aliases.
- Function imports: read the body, identify the business effect (status update, calculation, external call), translate to BDEF action with appropriate behavior pool method.
SAPRead(type="CLAS", name="<DPC_EXT>", method="<METHOD_NAME>")
For mostly-trivial methods, this is a 30-line read. For complex business logic, expect 100+ lines and plan to rewrite carefully.
3c. Compile into a behavior summary
=== Extracted behavior ===
Read-only (drop, free in RAP managed):
PROJECTSET_GET_ENTITYSET, PROJECTSET_GET_ENTITY,
TASKSET_GET_ENTITYSET, TASKSET_GET_ENTITY,
TIMEENTRYSET_GET_ENTITYSET, TIMEENTRYSET_GET_ENTITY
Function imports → BDEF actions:
ApproveProject(ProjectId) ➜ static action ApproveProject parameter $self
sets Status='A' + admin fields, returns Project
(legacy did UPDATE + COMMIT WORK + reread; RAP managed
handles persistence — no manual COMMIT needed)
Custom create/update/delete: none (read-only service)
If _CREATE_ENTITY / _UPDATE_ENTITY / _DELETE_ENTITY were present, list them with notes
on what would become BDEF determinations / validations.
Phase 4 — Read underlying tables
For each table identified in Phase 2 (e.g. ZDM_PROJECT, ZDM_TASK, ZDM_TIMEENTRY):
SAPRead(type="TABL", name="<table>")
Record:
- Field list with ABAP types + lengths
- Key fields
- Admin fields convention (classic
ERNAM/ERDAT/ERZET/AENAM/AEDAT/AEZETvs modernabp_*) - Foreign-key references (the
with foreign keyclauses)
The tables stay as-is — the new RAP CDS views read from them.
Phase 5 — Design plan + user approval
Before any write operation, present the complete RAP design as a design plan, get explicit user approval, then proceed.
5a. Naming
Legacy → New RAP
ZDM_PROJECT (table, untouched) ─► ZR_DM_PROJECT (CDS root view entity)
ZC_DM_PROJECT (CDS projection view, exposed via service)
ZI_DM_PROJECT (BDEF for root)
ZDM_TASK (table, untouched) ─► ZR_DM_TASK + ZC_DM_TASK
ZDM_TIMEENTRY (table, untouched) ─► ZR_DM_TIMEENTRY + ZC_DM_TIMEENTRY
(no SEGW project on the new side) ─► ZUI_DM_PROJECTS (service definition — exposes ZC_*)
ZUI_DM_PROJECTS_O4 (service binding, OData V4 UI)
ZBP_DM_PROJECT (behavior pool class for actions)
5b. Composition tree
root: ZR_DM_PROJECT
composition [0..*] of ZR_DM_TASK as _Tasks
composition [0..*] of ZR_DM_TIMEENTRY as _TimeEntries
Maps the SEGW associations 1:1 — except now they're compositions (parent owns children), which gives RAP managed lifecycle semantics for free.
5c. Action
ZI_DM_PROJECT (BDEF):
define behavior for ZR_DM_PROJECT alias Project
persistent table zdm_project
lock master
authorization master ( instance )
etag master last_changed_at
{
field ( readonly ) ProjectId;
action approve_project result [1] $self; ← matches SEGW ApproveProject
}
The approve_project action → handler in ZBP_DM_PROJECT → sets status='A' + admin fields
via the framework's MODIFY ENTITIES interface (no manual UPDATE + COMMIT).
5d. Service binding URL
Old (SEGW V2): /sap/opu/odata/sap/<legacy_service>
New (RAP V4): /sap/opu/odata4/sap/zui_dm_projects_o4/srvd_a2x/sap/zui_dm_projects/0001
(or the FLP-bound URL if the service binding is published as UI.)
5e. Show + ask
Print the full plan to the user. Wait for explicit "yes, proceed" or modifications. If the user wants changes (different naming, draft enabled, different RAP scenario), revise and re-present.
Non-interactive mode. If the user's initial prompt already supplied the Phase-5 equivalent inputs (legacy service, target package, transport, scenario, draft on/off) AND said "run end-to-end" / "don't stop for review", skip the rhetorical "yes, proceed?" gate. Print the plan as a manifest and advance directly to Phase 5f / Phase 6. Only stop if Phase 5 surfaced a genuine conflict (missing input, contradictory scenario flags, etc.). See Run 6 findings — full-chain automations otherwise hit a phantom gate.
5f. Lock the artifact list as a Phase-6 contract
Before leaving Phase 5, the skill MUST emit an artifact-list contract that Phase 6 will echo back with status. Print this verbatim:
=== Phase 6 will create + activate these artifacts (Phase 5 → Phase 6 contract) ===
Roots (CDS): [ ] ZR_DM_PROJECT [ ] ZR_DM_TASK [ ] ZR_DM_TIMEENTRY
Projections (CDS): [ ] ZC_DM_PROJECT [ ] ZC_DM_TASK [ ] ZC_DM_TIMEENTRY
(projections are MANDATORY — service binding never exposes roots directly)
Draft tables (TABL): [ ] ZDM_PROJECT_D [ ] ZDM_TASK_D [ ] ZDM_TIMEENTRY_D
(only when draft is on — based on Phase 5d)
BDEFs: [ ] ZR_DM_PROJECT (root behavior) [ ] ZC_DM_PROJECT (projection behavior)
Behavior pool (CLAS): [ ] ZBP_DM_PROJECT (empty shell — generate_behavior_implementation will populate)
Service definition: [ ] ZUI_DM_PROJECTS (exposes projections, NOT roots)
Service binding: [ ] ZUI_DM_PROJECTS_O4 (OData V4 UI)
Manual steps remaining after Phase 6 build:
[ ] /IWFND/MAINT_SERVICE — register V4 service group ZUI_DM_PROJECTS_O4 (else 403 at runtime)
Phase 6 must walk through this list at the end and print the same checkbox list with each
item marked ✓ or ✗ (with the failing tool call args + response on ✗). Do NOT declare
Phase 6 done until every box is ✓ or explicitly waived by the user.
Phase 6 — Build the RAP stack
6a. Reset existing artifacts (idempotent re-runs)
If <target_package> package contains objects from a previous run, delete them first:
SAPRead(type="DEVC", name="<target_package>")
For each object in reverse dependency order (SRVB → SRVD → BDEF → DDLS_C → DDLS → CLAS → TABL):
SAPWrite(action="delete", type="<type>", name="<name>", transport="<transport>")
(SAP recommends releasing the transport before re-running, but for resettable demos we just
reuse <transport> — it's fine.)
Plus a TADIR cross-check (Run 1 found a stub draft table sitting in a different transport
that the package scan missed; Run 6 found that ADT 404s can coexist with TADIR ghost rows —
the "split-brain" failure mode). On ARC-1 ≥ 0.9.5 (PR #270), the canonical reset truth
comes from source="both" mode, which queries ADT and DB TADIR in one call and emits a
splitBrain warning array for any divergent names:
SAPSearch(searchType="tadir_lookup",
source="both",
names=["ZDM_PROJECT_D","ZDM_TASK_D","ZDM_TIMEENTRY_D",
"ZR_DM_PROJECT","ZR_DM_TASK","ZR_DM_TIMEENTRY",
"ZC_DM_PROJECT","ZC_DM_TASK","ZC_DM_TIMEENTRY",
"ZI_DM_PROJECT_BEH","ZI_DM_TASK_BEH","ZI_DM_TIMEENTRY_BEH",
"ZBP_DM_PROJECT","ZUI_DM_PROJECTS","ZUI_DM_PROJECTS_O4"])
Read the response in this order:
results— names found via ADT discovery (the "is the object actually live" question). Empty = the package is clean from ADT's perspective.splitBrain— names where ADT and DB disagree (the "ghost" cases). For each, the array entry tells you which source saw it. The pragmatic interpretation: if a name is in DB-only (TADIR row, no ADT object), it's a ghost — treat as already absent for the purposes ofSAPWrite create. If it's in ADT-only (rare), there's a stale ADT cache —SAPSearchagain after a few seconds.
source="both" requires sql scope (the DB leg uses the free-SQL path). If your profile
doesn't have it, fall back to source="adt" (default) for the live check + SAPQuery for
a broader package sweep:
SAPQuery(action="sql", sql="SELECT obj_name, object, devclass, korrnum FROM tadir
WHERE devclass = '<target_package>' AND obj_name LIKE 'Z%'")
If a planned name appears in a different package, it's a leftover stub — delete via its actual transport before proceeding.
6a-gen. (Optional) Seed the root BO via SAP's official ABAP generator
If the official SAP ABAP MCP server is connected alongside ARC-1 (ships with ABAP Development
Tools for VS Code; enabled in Eclipse ADT 3.60+; appears as the abap-mcp server), its
Generate ABAP Repository Objects framework can scaffold the root managed+draft BO for you.
It does not replace this skill — Phases 1–5 (discovery + translation) and most of Phase 6 are
still ARC-1's job, because the generator is single-entity and one-shot:
"a maximum of one entity, no compositions or hierarchies" and "not intended for post-generation — subsequent changes require manual developer intervention."
So it fits a SEGW migration only when the legacy service is effectively a single root entity,
managed+draft, whose table carries the modern timestamp fields last_changed : abp_lastchange_tstmpl
and local_last_changed : abp_locinst_lastchange_tstmpl. Most SEGW services are multi-entity with
classic ERDAT/AEDAT admin fields and function imports → the generator does not apply; use the
manual build (6b) as normal. Where it does apply, it only seeds the root; ARC-1 still adds the
children, compositions, actions, and field mappings afterward (the generator can't, being one-shot).
If you use it:
- Probe. No
abap_generators-list_generatorstool (theabap-mcpserver) → skip to 6b, build manually. - Resolve the ID — release-specific, never hardcode. Call
abap_generators-list_generators, match the "OData UI Service" generator by display name, use the returnedid(proven live:uiserviceon SAP_BASIS 758 / S4 2023;ui-serviceon 816 / ABAP Platform 2025; 816 also hasx-ui-service"from scratch"). Not listed → skip to 6b. - Schema → generate.
abap_generators-get_schema(generatorId, packageName="<target_package>", referencedObjectType="TABL", referencedObjectName="<root table, e.g. ZDM_PROJECT>"), fill every required field from the returned schema, thenabap_generators-generate_objects(generatorId, …). This is a mutation — target<target_package>+<transport>, same guardrails as any write. - Hand back to ARC-1. Continue at Step 2 onward in 6b for the children/compositions, the
projection contract level, the action (
approve_project) + handler body, and activation — adapting the names the generator chose for the root. Reconcile its root CDS/BDEF naming with this skill'sZR_*/ZC_*/ZI_*_BEHplan, or accept the generator's names and update Phase 5f's contract.
State which path you took so the run stays auditable. When in doubt, prefer the manual build (6b) — it is the proven path for the multi-entity SEGW shape and never depends on a second MCP server.
6b. Build order — strict, no improvisation
The order below was learned the hard way (Run 1, calls #22–33). Do not reorder. Each step prevents a specific failure mode in the next.
Step 1 — Draft tables (only if Phase 5d.draft = ON)
SAPWrite(action="batch_create", objects=[
{ type: "TABL", name: "ZDM_PROJECT_D", source: "<draft table source — see template below>", package: "<target_package>", transport: "<transport>" },
{ type: "TABL", name: "ZDM_TASK_D", source: "<...>", package: "<target_package>", transport: "<transport>" },
{ type: "TABL", name: "ZDM_TIMEENTRY_D", source: "<...>", package: "<target_package>", transport: "<transport>" }
])
SAPActivate(action="activate", objects=[ {type:"TABL", name:"ZDM_PROJECT_D"}, ... ])
Draft table template — three rules learned in Run 2:
- Field names use BO-alias casing without underscores (
projectid,startdate, notproject_id,start_date). ABAP normalizes BDEF aliases to uppercase no-underscore; draft binding looks up by that name, not by the active table's snake_case. creationtimestampandlastchangedstampcolumns required fortotal etag+etag masterclauses in the BDEF to bind.@AbapCatalog.enhancement.category : #NOT_EXTENSIBLEis mandatory — without it the TABL save fails on 7.58.
@EndUserText.label : 'Demo: Project (draft shadow)'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #ALLOWED
define table zdm_project_d {
" Field names match the BO aliases (no underscore) — required by RAP draft binding.
" The active table ZDM_PROJECT can keep its snake_case names; the BDEF mapping bridges
" them. The draft table has no mapping clause, so its names must align with the BO directly.
key client : abap.clnt not null;
key projectid : abap.char(10) not null;
title : abap.char(100);
description : abap.char(255);
status : abap.char(1);
startdate : abap.dats;
enddate : abap.dats;
erdat : abap.dats;
erzet : abap.tims;
ernam : abap.char(12);
aedat : abap.dats;
aezet : abap.tims;
aenam : abap.char(12);
" Required for `total etag CreationTimeStamp` + `etag master LastChangedStamp` in BDEF.
" Computed in CDS root view via dats_tims_to_tstmp(...); persisted on draft instances.
creationtimestamp : abap.dec(15,0);
lastchangedstamp : abap.dec(15,0);
include sych_bdl_draft_admin_inc;
}
ZDM_TASK_D mirrors with taskid + projectid (FK), all task-specific columns aliased
no-underscore. ZDM_TIMEENTRY_D mirrors with entryid + taskid + projectid (FKs).
Step 2 — CDS roots (ZR_DM_*)
Top-down: parent first, then children. Use composition [0..*] of <child> as <name> —
no on clause for managed scenario.
If your legacy DPC_EXT did Aedat → Erdat fallback (run 1 found this), bake it into the root
view's timestamp computation. On 7.58 use @Semantics.systemDateTime.createdAt,
lastChangedAt, and localInstanceLastChangedAt — the last annotates the local-instance
etag field (abp_locinst_lastchange_tstmpl) that a managed-draft BO needs, and is exactly what
SAP's own RAP generator emits on 758. Verified live on S/4HANA 2023: standalone views with
localInstanceLastChangedAt and with @Semantics.businessDate.from/to both activate. Only
genuinely old releases (< 7.55, before draft support) lack these — do NOT strip them on 7.58.
@Semantics.systemDateTime.createdAt : true
dats_tims_to_tstmp(
zdm_project.erdat,
zdm_project.erzet,
abap_system_timezone($session.client, 'NULL'),
$session.client,
'NULL'
) as CreationTimeStamp,
@Semantics.systemDateTime.lastChangedAt : true
dats_tims_to_tstmp(
case zdm_project.aedat when '00000000' then zdm_project.erdat else zdm_project.aedat end,
case zdm_project.aedat when '00000000' then zdm_project.erzet else zdm_project.aezet end,
abap_system_timezone($session.client, 'NULL'),
$session.client,
'NULL'
) as LastChangedStamp
Activation order for composition CDS. The default
batch_createactivates each object inline in array order, so the parentcomposition [0..*] of ZR_DM_TASK as _Tasksactivates beforeZR_DM_TASKexists, hitting "data sourceZR_DM_TASKdoes not exist" (Run 6 reproducer). Three safe paths, pick whichever fits the run:
batch_createwithactivateAtEnd: true(ARC-1 ≥ 0.9.5, PR #270). Writes inactive drafts for every object, then issues one terminalactivateBatchso SAP's activator resolves the cross-references in a single pass. This is the recommended pattern when ARC-1 supports it — single tool call, no recovery dance:SAPWrite(action="batch_create", activateAtEnd: true, objects=[...])- Per-file
SAPWrite(action="create")for each root, then a singleSAPActivate(action="activate", objects=[...])batch at the end (after every DDL source has landed). Works on every ARC-1 release.- Manual bottom-up:
SAPWrite createparent →SAPActivatechild first → activate parent last. More tool calls; rarely needed.The same fix applies to Step 3 projections (composition chain mirrored). Activate the projection trio together at the end, not inline.
Step 3 — CDS projections (ZC_DM_*) — DO NOT SKIP
This is where Run 1 silently dropped the layer. A provider contract error here means
"create projections", NOT "remove the contract".
Critical: provider contract transactional_interface goes on the root projection
ONLY (ZC_DM_PROJECT). Child projections (ZC_DM_TASK, ZC_DM_TIMEENTRY) use bare
as projection on … and inherit the BO contract via redirected to parent. Putting
the contract on children yields "inappropriate provider contract on …" on activation
(Run 2).
Root projection — WITH contract
Pick the contract based on scenario:
- Managed + draft + CUD (this skill's default):
provider contract transactional_query(Run 3 evidence on 7.58: combining projection BDEF +use draftrejectstransactional_interface). - Read-mostly, no draft, no CUD:
provider contract transactional_interface.
@AccessControl.authorizationCheck : #NOT_REQUIRED
@EndUserText.label : 'Demo project — UI projection (root)'
@Metadata.allowExtensions: true
define root view entity ZC_DM_PROJECT
provider contract transactional_query
as projection on ZR_DM_PROJECT
{
key ProjectId,
Title,
Description,
Status,
StartDate,
EndDate,
Erdat, Erzet, Ernam, Aedat, Aezet, Aenam,
CreationTimeStamp,
LastChangedStamp,
_Tasks : redirected to composition child ZC_DM_TASK
}
Child projections — WITHOUT contract, with redirected to parent
@AccessControl.authorizationCheck : #NOT_REQUIRED
@EndUserText.label : 'Demo task — UI projection (child)'
@Metadata.allowExtensions: true
define view entity ZC_DM_TASK
as projection on ZR_DM_TASK
{
key TaskId,
ProjectId,
Title, Description, Status, Priority, DueDate, AssignedTo, EstimatedHours,
Erdat, Erzet, Ernam, Aedat, Aezet, Aenam,
_Project : redirected to parent ZC_DM_PROJECT,
_TimeEntries : redirected to composition child ZC_DM_TIMEENTRY
}
Same shape for ZC_DM_TIMEENTRY (no contract, _Task : redirected to parent ZC_DM_TASK).
Note
define view entity(notdefine root view entity) on children — only the BO root hasdefine root view entity.
Step 4 — Empty behavior pool (skeletons created automatically by Step 7)
Write only the empty global class shell. The local handler classes (lhc_project,
lhc_task, lhc_timeentry) are no longer pre-created here — Step 7's
SAPWrite action=generate_behavior_implementation (PR-C, ARC-1 ≥ post-2026-05-10)
auto-creates them when missing, then injects every required handler signature and stub
in one call. Run 2's mandatory ADT-paste pause for CCDEF/CCIMP is gone.
SAPWrite(action="create", type="CLAS", name="ZBP_DM_PROJECT",
source="CLASS zbp_dm_project DEFINITION
PUBLIC ABSTRACT FINAL
FOR BEHAVIOR OF zr_dm_project.
ENDCLASS.
CLASS zbp_dm_project IMPLEMENTATION.
ENDCLASS.",
description="Behavior pool for ZR_DM_PROJECT BO",
package="<target_package>", transport="<transport>")
SAPActivate(type="CLAS", name="ZBP_DM_PROJECT")
Expected activation behavior. The
SAPActivatehere may return "no behavior definition forzr_dm_project" — that's expected and OK. The BDEF doesn't exist yet; it's created in Step 5. Don't treat this as a real failure. The class will activate cleanly once the BDEF is in place; Step 7 (generate_behavior_implementation) re-activates the class with handlers anyway. Either:
- Skip activation here (write only) and rely on Step 7 to activate.
- Or accept the warning and re-activate the class as part of Step 10's batch activate.
That's it. No CCDEF/CCIMP write here. No ADT pause. Continue to Step 5.
Step 5 — Root BDEF with the managed-with-draft rules baked in
Run 1 calls #22–31 discovered ten managed-with-draft rules the hard way. Bake them in:
managed implementation in class zbp_dm_project unique;
strict ( 2 );
with draft;
define behavior for ZR_DM_PROJECT alias Project
persistent table zdm_project
draft table zdm_project_d
lock master
total etag CreationTimeStamp " <- required by `with draft`
etag master LastChangedStamp
authorization master ( instance )
{
create;
update;
delete;
field ( readonly : update ) ProjectId;
field ( readonly ) CreationTimeStamp, LastChangedStamp;
" composition associations: only `create` is allowed in the inline block
association _Tasks { create; with draft; }
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare; " <- 'determine' keyword required, not just 'draft action'
action approve_project result [1] $self; " <- DO NOT remove on activation failure;
" Step 7 (generate_behavior_implementation)
" auto-creates the lhc_project skeleton and
" injects the handler in one call.
mapping for zdm_project { … all field maps … }
}
define behavior for ZR_DM_TASK alias Task
persistent table zdm_task
draft table zdm_task_d
lock dependent by _Project " <- not _Project of root; use the cross-BO assoc you added in roots
etag dependent by _Project
authorization dependent by _Project
{
update; delete;
field ( readonly : update ) TaskId, ProjectId; " <- lock-by reference fields MUST be readonly
association _Project { with draft; }
association _TimeEntries { create; with draft; }
" NO draft Edit/Activate/Discard/Resume — those belong on the root only.
mapping for zdm_task { … }
}
define behavior for ZR_DM_TIMEENTRY alias TimeEntry
persistent table zdm_timeentry
draft table zdm_timeentry_d
lock dependent by _Project " <- not by _Task — chain must reach a `lock master`
etag dependent by _Project
authorization dependent by _Project
{
update; delete;
field ( readonly : update ) EntryId, TaskId, ProjectId;
association _Project { with draft; }
association _Task { with draft; }
mapping for zdm_timeentry { … }
}
If TimeEntry can only reach _Task and not _Project, you must add a cross-BO
association to ZR_DM_PROJECT as _Project in ZR_DM_TIMEENTRY (root view) — the lock
chain has to terminate at the entity that holds lock master.
Step 6 — Projection BDEF
Declares the behavior alias for the projection. Run 6 found that 7.58 enforces two non-obvious syntax rules in projection BDEFs — bake them in:
use draft;at the top (not inside the body) when the root BDEF declaredwith draft. Without it,use action Edit/Activate/Discard/Resume/Prepareis rejected.- Inside
use association _X { ... }blocks, write bare operation names (create;), notuse create;— theusekeyword belongs at the top level only.
Canonical projection BDEF for the root projection (managed + draft + CUD scenario):
projection;
strict ( 2 );
use draft;
define behavior for ZC_DM_PROJECT alias Project
use etag
{
use create;
use update;
use delete;
use action Edit;
use action Activate;
use action Discard;
use action Resume;
use action Prepare;
use action approve_project;
use association _Tasks { create; } " <- bare 'create;' inside, NOT 'use create;'
}
Child projections (ZC_DM_TASK, ZC_DM_TIMEENTRY) need their own projection BDEF blocks
in the same DDLS or as separate definitions, each using
use association _Project / _Task to expose the upward composition for the lock-master
chain. Bare create; inside association bodies, same rule.
Step 7 — Generate the behavior implementation (one call, fully autonomous)
SAPWrite(action="generate_behavior_implementation", type="CLAS", name="ZBP_DM_PROJECT")
That single call (PR-C, ARC-1 ≥ post-2026-05-10) does:
- Reads class metadata → extracts
<class:rootEntityRef>to auto-discover the bound BDEF (no need to passbdefName). - Cross-validates that MAIN's
FOR BEHAVIOR OF zr_dm_projectand the BDEF'smanaged implementation in class zbp_dm_project uniqueagree. Refuses to mutate on mismatch. - Calls the same scaffold engine
scaffold_rap_handlersuses, withautoApply=true:- Auto-creates missing
lhc_<alias>skeletons (CCDEF + CCIMP). - Injects
METHODS …signatures for every action / determination / validation / authorization the BDEF requires. - Injects empty
METHOD … ENDMETHOD.stubs in CCIMP.
- Auto-creates missing
- Writes CCDEF + CCIMP via the PR #257 include= path under one stateful lock.
- Activates the class.
Returns a structured JSON report including discovery, validation, scaffoldChanged,
counts, and activation.success. If activation fails with the well-known "Local classes
of CL_ABAP_BEHAVIOR_HANDLER…" stale-active coupling, the response includes a guided
activation.hint with concrete recovery options instead of throwing — the just-written
CCDEF/CCIMP source remains useful for both recovery paths.
If activation fails for any non-obvious reason, run SAPDiagnose(action="object_state", type="CLAS", name="ZBP_DM_PROJECT") (PR #254) to see active vs inactive divergence per
include before retrying. It surfaces the exact include that's out of sync without dumping
raw source.
Lower-level alternative: if you need to scaffold against an existing populated class
or want a dry-run-style preview without auto-activating, use scaffold_rap_handlers
directly with autoApply=true|false and bdefName explicit. Prefer
generate_behavior_implementation for fresh behavior pools.
Step 8 — Fill action body via edit_method
SAPWrite(action="edit_method", type="CLAS", name="ZBP_DM_PROJECT",
method="lhc_project~approve_project",
source="
METHOD approve_project.
READ ENTITIES OF zr_dm_project IN LOCAL MODE
ENTITY Project FIELDS ( ProjectId Status Aedat Aezet Aenam )
WITH CORRESPONDING #( keys )
RESULT DATA(projects).
MODIFY ENTITIES OF zr_dm_project IN LOCAL MODE
ENTITY Project
UPDATE FIELDS ( Status Aedat Aezet Aenam )
WITH VALUE #( FOR p IN projects (
%tky = p-%tky
Status = 'A'
Aedat = sy-datum
Aezet = sy-uzeit
Aenam = sy-uname
) ).
READ ENTITIES OF zr_dm_project IN LOCAL MODE
ENTITY Project ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(updated).
result = VALUE #( FOR u IN updated ( %tky = u-%tky %param = u ) ).
ENDMETHOD.",
transport="<transport>")
(No COMMIT WORK — the framework saves automatically on action commit.)
Step 9 — Service definition + binding
SAPWrite(action="batch_create", objects=[
{ type:
…(truncated)