Debug Command — Specification

Interactive ABAP debugger via SAP ADT REST API.

Overview

The debug command exposes the same ADT debugger API that Eclipse ADT uses, directly from the terminal. It supports two usage modes:

  • Human REPL modedebug attach blocks and enters an interactive readline session
  • AI / scripting mode — individual stateless sub-commands with --json output, backed by a persisted daemon process

Usage

abapgit-agent debug <subcommand> [options]

Breakpoint Management:
  set     --files <file>:<n>[,...]       Set breakpoints from file paths (e.g. abap/zcl_foo.clas.abap:42)
  set     --objects <name>:<n>[,...]     Set breakpoints from object names (e.g. ZCL_FOO:42)
  set     --object <name> --line <n>     Set a single breakpoint (legacy form)
  set     ... --include <type>           Target a class sub-include (testclasses|locals_imp|locals_def)
  list                                   List all breakpoints
  delete  --id <id>                      Delete a breakpoint by ID
  delete  --all                          Delete all breakpoints

  All breakpoint commands accept --json for machine-readable output.

Debug Session (Human REPL mode):
  attach  [--timeout <s>]                Attach and enter interactive REPL (default timeout: 30s)

Debug Session (AI / scripting mode):
  attach  --json [--timeout <s>]         Attach, wait for breakpoint, emit JSON, start daemon
  step    --type over|into|out|continue [--json]
  vars    [--name <var>] [--json]
  vars    --expand <var> [--json]        Drill into a table / structure / field symbol
                                           --expand LT_TABLE              all rows
                                           --expand LT_TABLE[N]           one row (fields)
                                           --expand LT_TABLE[N]->FIELD    one field in a row
                                           --expand '<FS>'                field symbol (all fields)
                                           --expand '<FS>'->FIELD         one field of a FS
  stack   [--json]
  terminate [--json]

Note: The active session is loaded automatically from the daemon state
file written by `attach --json`. No session ID needs to be passed.

REPL Commands (Human Mode)

When debug attach is called without --json, an interactive readline REPL starts after the breakpoint is hit:

Command Aliases Action
s step Step into
n next Step over
o out Step out
c continue Continue execution (releases work process)
v vars Show variables
x <var> expand <var> Drill into a table, structure, or field symbol (x LT_TABLE, x LT_TABLE[1], x '<FS>')
bt stack Show call stack
q quit Detach debugger (program continues running)
kill Terminate the running program (hard abort)
h help Show help

AI / Scripting Mode — Daemon IPC

When attach --json is called, a background daemon (debug-daemon.js) is spawned. The daemon holds the stateful ADT HTTP connection open and listens on a Unix socket. All subsequent step, vars, stack, and terminate commands communicate with the daemon via that socket rather than opening new HTTP sessions.

Four best-practice rules for scripted use:

  1. Wait for "Listener active" in the attach output before firing the trigger — attach --json prints this marker to stderr the moment the long-poll POST is about to reach ADT. A blind sleep is not reliable under system load
  2. Keep the trigger process alive in the background for the entire debug session
  3. Always finish with step --type continue — releases the frozen ABAP work process

Example workflow:

# Step 1 — start listener (rule 1: wait for "Listener active" before firing trigger)
abapgit-agent debug attach --json > /tmp/attach.out 2>&1 &
until grep -q "Listener active" /tmp/attach.out 2>/dev/null; do sleep 0.3; done
sleep 1   # brief extra window for the POST to reach ADT

# Step 2 — fire trigger, keep alive (rule 2)
abapgit-agent inspect --files abap/zcl_my_class.clas.abap > /tmp/trigger.out 2>&1 &

# Step 3 — poll for {"session":...}
SESSION=""
for i in $(seq 1 30); do
  sleep 0.5
  SESSION=$(grep -o '"session":"[^"]*"' /tmp/attach.out 2>/dev/null | head -1 | cut -d'"' -f4)
  [ -n "$SESSION" ] && break
done

# Step 4 — use debug commands (session loaded automatically)
abapgit-agent debug stack --json
abapgit-agent debug vars --json
abapgit-agent debug step --type over --json

# Step 5 — always release the work process (rule 3)
abapgit-agent debug step --type continue --json

ADT API Endpoints

All endpoints are under /sap/bc/adt/debugger/.

Operation Method Endpoint
Set/sync breakpoints POST /sap/bc/adt/debugger/breakpoints
List breakpoints GET /sap/bc/adt/debugger/breakpoints?clientId=<id>
Delete breakpoint DELETE /sap/bc/adt/debugger/breakpoints/<id>
Wait for hit POST /sap/bc/adt/debugger/listeners
Attach POST /sap/bc/adt/debugger?method=attach
Step POST /sap/bc/adt/debugger?method=stepOver\|stepInto\|stepReturn\|stepContinue
Get stack POST /sap/bc/adt/debugger?method=getStack
Get variables (child IDs) POST /sap/bc/adt/debugger?method=getChildVariables
Get variables (values) POST /sap/bc/adt/debugger?method=getVariables
Terminate POST /sap/bc/adt/debugger?method=terminateDebuggee

Breakpoint XML Format

Critical: The correct format was verified against abap-adt-api. Earlier attempts using adtdbg: namespace or PUT method failed silently.

<?xml version="1.0" encoding="UTF-8"?>
<dbg:breakpoints xmlns:dbg="http://www.sap.com/adt/debugger"
                 scope="external"
                 debuggingMode="user"
                 requestUser="MYUSER"
                 terminalId="ABAPGIT-AGENT-CLI"
                 ideId="ABAPGIT-AGENT-CLI"
                 systemDebugging="false"
                 deactivated="false">
  <syncScope mode="full"></syncScope>
  <breakpoint xmlns:adtcore="http://www.sap.com/adt/core"
              kind="line"
              clientId="ABAPGIT-AGENT-CLI"
              skipCount="0"
              adtcore:uri="/sap/bc/adt/oo/classes/zcl_my_class/source/main#start=42"/>
</dbg:breakpoints>

Key rules:

  • Method: POST to /sap/bc/adt/debugger/breakpoints (not PUT)
  • Content-Type / Accept: application/xml
  • Root namespace: dbg: (http://www.sap.com/adt/debugger) — not adtdbg:
  • <syncScope mode="full"> is required — this is the synchronize model: the full desired list is sent each time; absent breakpoints are removed server-side
  • URI format for classes: /sap/bc/adt/oo/classes/<lowercase>/source/main#start=<line>
    • The /source/main suffix is required — without it ADT returns errorMessage="Cannot create a breakpoint at this position"
    • The #start=<line> fragment carries the line number — there is no separate line element
  • URI format for programs: /sap/bc/adt/programs/programs/<lowercase>#start=<line>
  • kind="line" — not "breakpoint"
  • clientId scopes breakpoints to this tool (separate from Eclipse ADT breakpoints)

The response XML contains a <breakpoint id="..."> element with the server-assigned ID (or errorMessage if the line is not a valid breakpoint position, e.g. a comment or DATA declaration).

Object URI mapping (objectUri())

// ZCL_*/ZIF_*/YCL_*/YIF_* → OO class with /source/main
/sap/bc/adt/oo/classes/zcl_my_class/source/main

// With --include flag → OO class sub-include
/sap/bc/adt/oo/classes/zcl_my_class/includes/testclasses      // --include testclasses
/sap/bc/adt/oo/classes/zcl_my_class/includes/implementations  // --include locals_imp
/sap/bc/adt/oo/classes/zcl_my_class/includes/definitions      // --include locals_def

// Everything else → program
/sap/bc/adt/programs/programs/zmy_program

Class Sub-Include Breakpoints (--include)

Regular CM* methods live in the assembled class source (/source/main) and use global assembled-source line numbers. Unit test methods and local class methods live in separate ADT sub-includes that cannot be addressed through /source/main.

Use --include to route the breakpoint to the correct sub-include:

Flag value abapGit file ADT URI path segment Include type
testclasses .clas.testclasses.abap testclasses CCAU (unit test class)
locals_imp .clas.locals_imp.abap implementations CCIMP (local class implementations)
locals_def .clas.locals_def.abap definitions CCDEF (local class definitions)

Flag values mirror the abapGit file suffix — the same text users already know from the file name. The ADT URL path segment is different (locals_impimplementations) but this translation is handled internally.

Line numbers for sub-includes are section-local — they match the line numbers in the corresponding .clas.testclasses.abap / .clas.locals_imp.abap file, not the assembled source.

Finding the right line number: Use view --objects ZCL_MY_CLASS --full --lines. The output shows a ready-to-use debug set hint at each method header, with the --include flag already set:

  * ---- Section: Unit Test (from .clas.testclasses.abap) ----
  * ---- Method: SETUP — breakpoint: debug set --objects ZCL_MY_CLASS:12 --include testclasses ----
    10    METHOD setup.
    11      DATA lv_x TYPE i.
    12      mo_cut = NEW #( ).

  * ---- Section: Local Implementations (from .clas.locals_imp.abap) ----
  * ---- Method: ZIF_FOO~DO_SOMETHING — breakpoint: debug set --objects ZCL_MY_CLASS:5 --include locals_imp ----
     3    METHOD zif_foo~do_something.
     4      DATA lv_x TYPE i.
     5      lv_x = iv_input.
# Unit test method:
abapgit-agent debug set --objects ZCL_MY_CLASS:12 --include testclasses
# Local class method:
abapgit-agent debug set --objects ZCL_MY_CLASS:5 --include locals_imp
# Regular CM* method (no --include needed):
abapgit-agent debug set --objects ZCL_MY_CLASS:42

Breakpoint State (Local Persistence)

Because the ADT synchronize model requires sending the full desired list on every POST, the current list is persisted locally in a tmp file (/tmp/abapgit-debug-bp-<hash>.json) by debug-state.js. This allows set, delete, and list to be called as independent CLI invocations without a round-trip GET each time.

debug list first checks local state; falls back to GET /sap/bc/adt/debugger/breakpoints if local state is empty.

Listener Flow

The ADT listener is a long-polling POST that blocks until a breakpoint fires (or times out):

POST /sap/bc/adt/debugger/listeners
  ?debuggingMode=user
  &requestUser=MYUSER
  &terminalId=ABAPGIT-AGENT-CLI
  &ideId=ABAPGIT-AGENT-CLI
  &timeout=30
  • On timeout (no hit): returns 200 with empty body → poll again
  • On breakpoint hit: returns 200 with XML body containing <DEBUGGEE_ID>
  • On conflict (another listener active): returns 406 → wait 2s and retry

The terminalId and ideId in the listener must match the terminalId and ideId in the breakpoint registration. If they differ, the listener never receives the notification.

Attach

After the listener returns a DEBUGGEE_ID, attach to register as the active debugger for that work process:

POST /sap/bc/adt/debugger?method=attach
  &debuggeeId=<DEBUGGEE_ID>
  &dynproDebugging=true
  &debuggingMode=user
  &requestUser=MYUSER

Headers: X-sap-adt-sessiontype: stateful

Response contains debugSessionId="..." — this is used for all subsequent calls.

The X-sap-adt-sessiontype: stateful header is required on all session operations (attach, step, getStack, getVariables, terminate). Without it, each request may land on a different ABAP work process, causing noSessionAttached (T100KEY-NO=530) errors.

SAP’s ICM drops stateful session affinity after ~60 s of idle. In human REPL mode, startKeepalive() is called automatically after the REPL appears; it fires a getStack() every 30 s to keep the session warm. stopKeepalive() is called before detach()/terminate() to prevent a race between the keepalive timer and the closing request. In daemon (AI) mode the daemon’s own periodic IPC traffic keeps the session alive.

Variables — Two-Step Protocol

Getting variable values requires two POST calls:

Step 1 — getChildVariables: Returns the variable hierarchy (parent → child IDs).

POST /sap/bc/adt/debugger?method=getChildVariables
Content-Type: application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.ChildVariables
Accept:       application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.ChildVariables

Body: request children of @ROOT, @PARAMETERS, @LOCALS, @DATAAGING

Request body:

<?xml version="1.0" encoding="UTF-8" ?>
<asx:abap version="1.0" xmlns:asx="http://www.sap.com/abapxml">
  <asx:values><DATA><HIERARCHIES>
    <STPDA_ADT_VARIABLE_HIERARCHY><PARENT_ID>@ROOT</PARENT_ID></STPDA_ADT_VARIABLE_HIERARCHY>
    <STPDA_ADT_VARIABLE_HIERARCHY><PARENT_ID>@PARAMETERS</PARENT_ID></STPDA_ADT_VARIABLE_HIERARCHY>
    <STPDA_ADT_VARIABLE_HIERARCHY><PARENT_ID>@LOCALS</PARENT_ID></STPDA_ADT_VARIABLE_HIERARCHY>
    <STPDA_ADT_VARIABLE_HIERARCHY><PARENT_ID>@DATAAGING</PARENT_ID></STPDA_ADT_VARIABLE_HIERARCHY>
  </HIERARCHIES></DATA></asx:values></asx:abap>

Requesting @PARAMETERS and @LOCALS directly (in addition to @ROOT) is essential to get leaf variable IDs in a single round-trip. Without them, ADT only returns the group node IDs (@PARAMETERS, @LOCALS) but not their children (the actual variables).

The response HIERARCHIES table maps PARENT_ID → CHILD_ID. Extract CHILD_ID values whose PARENT_ID is @PARAMETERS, @LOCALS, @ROOT, or @DATAAGING, filtering out virtual group IDs (those starting with @).

Step 2 — getVariables: Get values for the leaf IDs from step 1.

POST /sap/bc/adt/debugger?method=getVariables
Content-Type: application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.Variables
Accept:       application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.debugger.Variables

The dataname= part of Content-Type is an ADT routing key — using the wrong value silently routes the request to the wrong handler.

Step 3 — Field symbol enrichment (source-parse): FIELD-SYMBOLS <FS> declarations are never returned by ADT’s getChildVariables hierarchy (ADT does not enumerate them regardless of group ID). getVariables() fetches the current frame’s ABAP source via getStack + the adtcore:uri source endpoint, extracts all FIELD-SYMBOLS <name> declarations with a regex, and requests them directly via getVariables. Assigned field symbols appear in the variable list and can be expanded like any other variable. Unassigned field symbols return an empty or unset value. This enrichment is best-effort — if the source fetch fails, normal variable results are still returned.

--expand — Drilling into Variables

debug vars --expand <name> resolves a multi-segment path through the variable hierarchy. The following forms are all supported:

Form Example What it returns
Whole table --expand LT_TABLE All rows ([1], [2], …)
Single row --expand LT_TABLE[N] All fields of row N
Field in row --expand LT_TABLE[N]->FIELD Value of FIELD in row N
Field symbol --expand '<FS>' All fields of the assigned structure
Field in FS --expand '<FS>'->FIELD Value of FIELD in the assigned structure
Structure field --expand LS_STRUC->FIELD Value of one field

The [N] row index and -> path separator normalisation is handled in cmdExpand() before routing:

  • LT_TABLE[N]LT_TABLE->[N] (routed through expandPath)
  • LT_TABLE[N]->FIELDLT_TABLE->[N]->FIELD (mid-path normalisation)
  • Single-segment names without [N] use getVariableChildren directly

Multi-segment paths (pathParts.length > 1) are dispatched to the background daemon via the expandPath IPC command, so the stateful ADT session is always used.

Stack

POST /sap/bc/adt/debugger?method=getStack&emode=_&semanticURIs=true

Response contains <stackEntry> elements with stackPosition, programName, includeName, line, eventName, stackType, and adtcore:uri attributes.

  • Skip stackType != "ABAP" frames (DYNP/screen frames are not useful)
  • Sort descending by stackPosition so frames[0] is top of stack
  • adtcore:uri contains the source URI with #start=<line> — can be used to fetch source context

Session Persistence (AI Mode)

When attach --json succeeds, both the sessionId and the daemon’s Unix socketPath are saved to /tmp/abapgit-debug-session-<hash>.json. All AI-mode commands (step, vars, stack, terminate) load the socket path automatically and communicate with the daemon via IPC.

The daemon auto-exits after 30 minutes of idle time or when terminate is called.

detach() — single stepContinue: When the REPL sends q, detach() issues one stepContinue. ADT returns HTTP 200 as soon as the WP resumes (regardless of whether it later hits another breakpoint — “still running” and “re-hit breakpoint” are indistinguishable in the response). Sending a second stepContinue to an already-running WP races with the program’s execution and can stall it mid-run (e.g. while a Code Inspector job is in flight). The REPL’s q handler deletes all breakpoints before calling detach() to prevent an immediate re-hit.

Files

File Purpose
src/commands/debug.js CLI subcommand routing, breakpoint management, attach/step/vars/stack/terminate
src/utils/debug-session.js DebugSession class — wraps all stateful ADT calls
src/utils/debug-state.js Persists active session ID, socket path, and breakpoint list to tmp files
src/utils/debug-repl.js readline-based interactive REPL for human mode
src/utils/debug-render.js Shared display helpers (variable list, source context) used by REPL and AI mode
src/utils/debug-daemon.js In-process IPC daemon — holds stateful ADT HTTP session; accepts commands via Unix socket
src/utils/adt-http.js ADT HTTP client (axios, keepAlive connection pool, CSRF, cookie jar, XML helpers)
tests/unit/debug-command.test.js Unit tests for debug command
tests/unit/adt-http.test.js Unit tests for AdtHttp
tests/integration/debug-repl-scenarios.sh Integration tests: REPL simple, REPL takeover (bash, local only)
tests/integration/debug-scripted-scenarios.sh Integration tests: scripted –json scenarios 3/4/5 (local only — requires TCP session affinity)

Prerequisites

  • ABAP system must have ICF node /sap/bc/adt/ activated (transaction SICF)
  • User needs S_ADT_RES authorization (ACTVT=16) for debugging
  • Breakpoints only fire for dialog work processes executing under the configured user; background jobs do not trigger the listener

Known Limitations

  • Breakpoints fire only for dialog work processes executing under the configured user
  • The #start=<line> line number in the URI must point to an executable ABAP statement — comments, blank lines, DATA declarations, and METHOD/ENDMETHOD lines are rejected with “Cannot create a breakpoint at this position”. Use view --objects ZCL_MY_CLASS --full --lines to find valid line numbers — the method header hint already skips non-executable lines and points directly to the first executable statement
  • Unit test methods and local class methods require --include testclasses / --include locals_imp with section-local line numbers (not assembled-source global lines); view --full --lines emits the correct hint for these sections automatically
  • FIELD-SYMBOLS <FS> variables are resolved via source-parse enrichment (see Variables section). Unassigned field symbols return an empty or unset value. Field symbols from ASSIGNING <FS> inside a LOOP are visible only when the loop body is the active frame
  • Debug scripted scenarios require TCP session affinity — the in-process daemon shares the same axios keepAlive connection for attach/getStack/step/vars. If a network proxy or load balancer between the client and SAP breaks TCP connection affinity, getStack returns HTTP 400 “Service cannot be reached”. The scripted scenarios (3/4/5) are skipped in CI for this reason but run locally (npm run test:debug:scenarios)

Back to top

Copyright © 2024-2026 abapGit Agent. Distributed under MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.