CLICLItypeA command you run, rather than a dependency you import.See every one →What this type is →independently maintainedindependently maintainedoriginPublished by one person or a small company outside SAP. Says nothing about quality; the readings say that.See every one →

abap-adt-cli

Pulls an ABAP package to local files over the ADT REST API in parallel, and pushes edited objects back under a transport — bulk sync rather than one object at a time in SE80 or Eclipse.

abap pull fetches a package and everything under it as ordinary files laid out the way SE80 shows them, one folder per object, with every text of an object (a class with its local classes and test classes) as its own file; .adt/manifest.json keeps what was pulled, from where, and its hashes. abap status reads that manifest offline to say what changed locally, abap status --remote checks whether anyone else changed the same objects on the server first, and abap push --transport <id> sends every edited object in one run, with --activate added to activate what was just pushed. A CDS view, its behavior definition and the class behind it never activate out of order. Editing one method records a LIMU METH transport entry rather than locking the whole class. Its own account: every command has been run against a live S/4HANA system, not only unit-tested, including a cyclic CDS dependency that fails if activated one at a time.

Facts

Last pushLicenceMIT

Where it sits

Lives in
Provides
Source controlABAPSource controlcapabilityKeeping source code and development objects in version control, comparing their revisions, and moving them between development environments. Managed delivery of changes belongs to [transport management](capability:change-promotion).Change promotionABAPChange promotioncapabilityMoving a change through the environments of a landscape, from development through test to production, and knowing which change has reached which environment. A [CI/CD pipeline](capability:ci-cd) builds and tests on each source change, [deployment](capability:deployment) puts a build onto its runtime, and getting source into git is [source control](capability:abap-source-control).Agent access to your codeABAPAgent access to your codecapabilityLetting a coding agent read the project or the system you actually have, reach its model, its objects and its checks, and act on it. In file-based environments that works out of the box; complex systems need dedicated access capabilities. Whatever the environment, the agent also needs project- and framework-specific context, so it understands the thing in front of it rather than guessing from what it remembers of its training data. The transport is incidental: an MCP server, a language server or a CLI answer the same question, and what differs between them is how much of the real thing the agent can see. Distinct from [MCP servers](capability:mcp-servers), which is a protocol adapter on your own running service, and from [agent guidance](capability:agent-guidance), which is knowledge handed over as files rather than read from the thing itself.
Requires
SAP S/4HANASAP S/4HANAoutside the atlasHaving an SAP system with ADT enabled to pull from and push to, which the README names as NetWeaver or S/4

Who is behind it

vaibhavgoel-github-1986vaibhavgoel-github-1986PersonMaintains this
In the atlas: maintains 1, contributes to 1
@vaibhavgoel-github-1986
maintainervaibhagoPersonContributes to this
In the atlas: contributes to 1
@vaibhago
1 contributor

Before choosing this

Conditions

PracticalFunctional boundary

A Python 3.10+ CLI, installed with pipx rather than through any SAP-provided package manager. Its own README lists what it does not do yet: where-used, syntax check and unit test runs — for those it says to reach for ADT/Eclipse or an MCP server. First release, v0.1.0, and no standalone LICENSE file in the repository — the MIT grant is in the README and in pyproject.toml's project metadata rather than its own file.

PolicySAP API Policy 4.2026a§ 1.1, 1.2, 2.2.2from Agent access to your codeAn MCP server calls ADT or another SAP API of a running system for an agent.
  • § 1.1 Published APIs. Published APIs are made available for the documented purposes identified by SAP, including integration, extensions and data exchange.
  • § 1.2 Non-Published APIs. Customer and third-party applications must not access non-published APIs except where documentation permits it or SAP otherwise authorizes it.
  • § 2.2.2 AI call sequences and data extraction. Outside SAP-endorsed architectures, data services or expressly identified pathways, the policy prohibits specified agentic API use and systematic or large-scale extraction.

An agent that plans and executes ADT calls is the case section 2.2.2 names. Check that the endpoints are published for the intended use, and that the design runs within a pathway SAP identifies for it.

SAP API Policy 4.2026a →

What it declares

Every artifact

Type
CLI
Origin
Independently maintained

As a CLI

Distributed through
PyPI

Connections4

Where abap-adt-cli runsBy its type: a CLI is run from a terminalcallsdocumentedrequiresYour terminal, actorYour terminalSAP S/4HANA, SAP solutionSAP S/4HANASAP solutionabap-adt-cli, moduleabap-adt-cli
SAPnot SAP's, or the project's ownstated in the documentdocumented only

README

quoted from vaibhavgoel-github-1986/abap-adt-cli under MIT, read
vaibhavgoel-github-1986/abap-adt-cli/README.mdOn GitHub

abap cli

Pull an ABAP package onto your laptop, edit it with whatever you like, push it back under the right transport. Built directly on the SAP ADT REST API — the same interface Eclipse/ADT uses, so nothing has to be installed in the SAP system.

abap pull ZMY_PACKAGE --dest ~/Documents/my-package
abap status --remote
abap push --transport DEVK900123

Why this exists

ABAP source lives behind SE80 and Eclipse. Getting a whole package in front of modern tooling — your editor, grep, git diff, an AI assistant — usually means copying it out one object at a time.

abap cli moves it in bulk, over HTTPS:

  • Pull a package in one command. Thousands of objects fetched in parallel and written as ordinary files, including the parts SE80 hides — a class arrives with its local classes and test classes as separate, editable files.
  • See what changed before you ship. abap status says which objects you touched; abap diff says what changed, line by line, and whether it was you or somebody working in SAP.
  • Push and activate in bulk. Every edited object goes in one run and they activate together, so you never work out the right order for a CDS view, its behavior definition and the class behind it.
  • Correct transport entries. Editing one method records one LIMU METH entry, not a lock on the whole class, so a colleague can work on the next method in their own transport.
  • Your source never passes through a language model. The transfer is plain HTTPS between your machine and SAP. An AI assistant reads the files from disk, so a large package costs no context window and nothing is paraphrased in transit.

Requirements

Python3.10 or newer
SAPADT enabled — standard on NetWeaver and S/4 — and a user with S_DEVELOP
OSmacOS, Linux, Windows
OptionalVS Code's code command on PATH, so --dest can add the folder to your workspace

The Python dependencies — httpx, typer, rich, keyring — are installed for you.

Install

pipx install "git+https://github.com/vaibhavgoel-github-1986/abap-adt-cli"

The two repositories hold the same code; the internal one is primary and the public one is a mirror. Install from whichever you can reach.

pipx keeps the CLI in its own virtualenv and puts abap on your PATH. Plain pip install works too. To pin a published version rather than the tip of the default branch, name the tag:

pipx install "git+https://github.com/vaibhavgoel-github-1986/abap-adt-cli@v1.6.4"

Afterwards abap update keeps it current — with one caveat if you installed from the internal mirror, see Versions and updating.

First-time setup

Three commands, once per system. init writes a connection profile, login puts the password in your OS keychain, ping proves both are right.

1. Describe the system. init prompts for anything you leave out:

$ abap init
System name (e.g. dev-100): dev-100
Host URL (https://host:port): https://sap.example.com:44300
SAP user: DEVELOPER
SAP client: 100
saved dev-100 to /Users/you/.abap-adt/config.json
next: abap login --system dev-100
PromptWhat it wantsWhere to find it
System nameany label you choose, later used as --system dev-100your own convention; <sid>-<client> reads well
Host URLthe ADT endpoint, scheme and port includedthe same host and port your Eclipse ADT project uses
SAP useryour SAP user nameSU3, or your logon screen
SAP clientthree digitsthe client you log on to

Add --insecure if the system serves a self-signed certificate, which is common on internal dev boxes — without it the connection fails at the TLS handshake. Every init makes that system the default, and re-running it with the same name edits the profile in place.

2. Store the password. It goes to the OS keychain, never to a file:

abap login

3. Check it answers, before waiting on a long pull:

$ abap ping
ADT alive on dev-100 client 100 as DEVELOPER  (24,118 bytes)

A failure here tells you which of the three things is wrong — host unreachable, credentials rejected, or ADT not enabled for your user. A host that is only reachable over VPN fails in about five seconds rather than hanging.

4. Pull a package.

abap pull ZEXAMPLE_PKG --dest ~/Documents/example-pkg

That is the whole setup. abap systems lists what you have configured and marks the default with *. Anything beyond one system — several systems, CI without a keychain, changing a password — is in Systems and credentials.

Quick start

Pull a package into a folder you choose. With --dest that folder is also added to your active VS Code workspace:

abap pull ZEXAMPLE_PKG --dest ~/Documents/example-pkg
cd ~/Documents/example-pkg
connected to dev-100, listing ZEXAMPLE_PKG and its sub-packages...
pulling 30 objects ━━━━━━━━━━━━━━━━━━━━ 74/74 0:00:02
pulled ZEXAMPLE_PKG from dev-100 - 30 objects in 38 files, 204,345 bytes in 2.3s

That brings down the named package and every package underneath it, because a package in SAP is usually a hierarchy. Pass --no-subpackages for the named package alone. See Sub-packages.

The bar counts requests, not objects, because one object can be several editable texts — a class is up to five. You now have ordinary files, laid out the way SE80 shows them, with every file of an object in one folder:

Class Library/Classes/ZCL_EXAMPLE_READER/
  zcl_example_reader.clas.abap
  zcl_example_reader.clas.testclasses.abap
Core Data Services/Data Definitions/ZCDS_I_EXAMPLE/
  zcds_i_example.ddls.asddls
Business Services/Service Definitions/ZSD_EXAMPLE_API/
  zsd_example_api.srvd.srvdsrv
.adt/manifest.json        # what was pulled, from where, and its hashes

Pass --flat if you would rather have every file in one src/ folder. See Layout.

Edit them however you like, then see what you touched — offline and instant:

$ abap status
ZEXAMPLE_PKG from dev-100, pulled 2026-09-14T17:37:49
  M  Core Data Services/Data Definitions/ZCDS_I_EXAMPLE/zcds_i_example.ddls.asddls

1 modified, 0 deleted

Check nobody else moved, confirm the blast radius, then send it:

abap status --remote              # did anyone change these on the server?
abap push --dry-run               # what would be sent - makes no ADT calls
abap push --transport DEVK900456
  M  Core Data Services/Data Definitions/ZCDS_I_EXAMPLE/zcds_i_example.ddls.asddls
  pushed Core Data Services/Data Definitions/ZCDS_I_EXAMPLE/zcds_i_example.ddls.asddls

1 object(s) pushed

Pushed objects stay inactive until activated. Add --activate and push activates exactly what it just wrote:

abap push --transport DEVK900456 --activate

Three options are worth knowing from the start:

Option
--system / -swhich system, when you have more than one
--traceprint every ADT request and response, secrets redacted
--helpper-command help. It must come after the command: abap pull --help

--trace is accepted on either side of the command:

abap push --transport DEVK900123 --trace
abap --trace push --transport DEVK900123

Commands

CommandPurpose
abap init / login / logout / systemsconnection profiles and credentials
abap pingcheck the endpoint is reachable
abap typesobject types that can be pulled and pushed
abap pull [PACKAGE]download a package, in parallel
abap pull [PACKAGE] --match / --type / --userdownload only part of one
abap statuslocally modified objects, offline
abap status --remotealso report what changed on the server
abap diffline-level differences against the server copy
abap pushupload changed objects
abap push --activate...and activate just those objects afterwards
abap delete NAME...delete objects from SAP and the workspace
abap transportsevery request you own, filtered by status and category
abap transports new "DESC"create a request
abap transports attr TR NAME=VALUEshow or set CTS attributes
abap transports delete TRdelete a request or task
abap transport TR listwhat a request contains
abap transport TR add NAME...add objects, whole or method-level
abap transport TR remove NAME...remove objects from a request
abap api METHOD SERVICE ENTITYcall an OData service on the system
abap versionwhich version is installed, and how
abap updateinstall the newest published release

Every command that talks to SAP also takes --system / -s and --trace.

Every command also carries its own reference: abap <command> --help prints what it does, the guard rails it applies and worked examples.

What each command is for
abap init, login, logout, systems, ping

Connection profiles and credentials. Passwords go to the OS keychain, never to a file. ping checks the endpoint answers before you wait on a long pull. Full detail: Systems and credentials.

abap types

The object types the installed build can pull and push, read straight from the registry the code uses. --all also lists what is skipped and why. Start here if you are unsure whether a package is fully covered. Full detail: Object types.

abap pull

Downloads a package and the packages below it into a folder, and writes a manifest describing what was fetched. Run it with no arguments inside an existing workspace to refresh in place, keeping the same system, layout, breadth and any --match / --type / --user narrowing. Full detail: Pulling.

abap status

Which objects you changed, compared against the hashes taken at pull time. Offline and instant. --remote also reports what changed in SAP since your pull, which is exactly what push will refuse. Full detail: Reviewing before you push.

abap diff

What changed, line by line, against the copy in SAP, and whether the change came from you, from the server, or both. The third case is a conflict. Full detail: Reviewing before you push.

abap push

Sends every changed file back, each to its own ADT endpoint, so a one-method edit stays a one-method transport entry. Add --activate to activate the whole batch in a single run. Always safe to rehearse with --dry-run. Full detail: Pushing and Activating.

abap delete

Removes objects from SAP and from the workspace. The one irreversible command, so it confirms first and only accepts objects the workspace already tracks. Full detail: Deleting.

abap transports

Every request you own, split into workbench and customizing, modifiable and released, with a filter for each. new creates one from a description, attr shows or sets its CTS attributes, and delete removes it. Full detail: Transports.

abap transport

Inspect a request, or move objects in and out of it. list prints the exact PGMID:TYPE:NAME form that add and remove accept. Full detail: Transports.

abap api

Call any OData service the system publishes, reusing your ADT logon. v2 Gateway and v4 RAP, any HTTP method, $metadata included. Full detail: Calling an OData service.

abap version, abap update

Which build is installed and how it was installed, and upgrading to the newest published release. Full detail: Versions and updating.

Pulling

A workspace is any folder containing .adt/manifest.json. The manifest records the package, the system, the layout, and one entry per file — the object's name, ADT type and ADT URI, which editable text of that object the file holds, and the SHA-256 of exactly what the server sent. Everything status and push do later is derived from it, which is why no git repository is required.

Where the files land depends on whether you pass --dest and whether you are already standing in a workspace:

CommandRun fromResult
abap pull ZMY_PACKAGEa plain foldercreates ./ZMY_PACKAGE/ (name upper-cased)
abap pull ZMY_PACKAGE --dest ~/Documents/projanywhereuses ~/Documents/proj exactly, then runs code --add on it
abap pulla workspacerefreshes that workspace in place
abap pull ZMY_PACKAGEthat same package's workspacerefreshes in place, never nests a copy
abap pull ZOTHERa different package's workspacecreates ./ZOTHER/ beside it

So the two forms differ in three ways:

abap pull ZMY_PACKAGE derives the folder name from the package, always underneath the current directory, and does not touch VS Code. Where you happen to be standing decides where the code lands.

abap pull ZMY_PACKAGE --dest <path> puts it at exactly that path — any name, anywhere on disk, absolute or relative — and after a successful pull runs code --add <path> to add the folder to the active VS Code workspace. Install VS Code's code command on your PATH to enable this; a missing command prints a note and does not affect the pull.

Re-running abap pull inside a workspace is a refresh, and reuses the package, system and layout from the manifest. It is deliberately conservative: if any tracked file is locally modified or deleted, it lists them and stops rather than overwriting your work. --force discards local changes and re-downloads.

OptionEffect
--dest / -dexact target folder, plus code --add
--match / -mobject name pattern, e.g. 'ZCL_EX*'
--type / -tcomma-separated ADT types, e.g. 'CLAS,DDLS'
--user / -uobject owner; defaults to you for $ packages, '*' means everyone
--se80 / --flatSE80 object tree (default), or one flat src/ folder; a refresh keeps whatever the manifest recorded
--subpackages / --no-subpackagesdescend the package hierarchy (default), or take the named package alone
--jobs / -jparallel requests, default 30
--force / -fdiscard local modifications and overwrite
--system / -spull from a system other than the default, or the one recorded in the manifest

Objects that fail to download are reported individually and do not abort the pull; everything else still lands.

Sub-packages

SAP applications are package hierarchies, and abap pull brings the whole hierarchy down by default:

$ abap pull ZEXAMPLE_SUITE
connected to dev-100, listing ZEXAMPLE_SUITE and its sub-packages...
5154 objects in 9 packages
  pulling 4812 objects ━━━━━━━━━━━━━━━━━━━━ 6701/6701 0:02:04
pulled ZEXAMPLE_SUITE from dev-100 - 4790 objects in 4881 files across 9 packages, ...

The listing endpoint does most of that on its own: SAP's package facet is already hierarchical, so asking for one package returns the objects of every package beneath it too. Walking the tree is therefore not what finds the children — it is what tells parent and child apart.

That is why --no-subpackages works by subtraction. It lists the sub-packages as well, then drops everything they account for, leaving only what sits directly in the package you named:

$ abap pull ZEXAMPLE_SUITE --no-subpackages
5046 objects in 1 package

An object listed by two packages is fetched once, and a cycle in the hierarchy terminates because each package is visited only once. --match, --type, --user and the chosen layout apply to the whole tree, and every object lands in the same workspace regardless of which package it came from — the manifest tracks objects, not packages.

The choice is recorded in the manifest, so a later bare abap pull refresh reaches exactly as far as the pull it repeats and can never silently widen.

A workspace pulled before 1.1.0 has no such record, and is refreshed as the single package it originally was. Pass --subpackages once to widen it deliberately:

abap pull ZMY_PACKAGE --subpackages --force
Layout

The default is an SE80-style tree. Each object gets a folder inside its type folder, and everything belonging to that object lives in it:

Class Library/Classes/ZCL_EXAMPLE_UTILS/
  zcl_example_utils.clas.abap
  zcl_example_utils.clas.locals_imp.abap
  zcl_example_utils.clas.testclasses.abap
Function Groups/ZFG_EXAMPLE/
  zfg_example.fugr.abap              # the group's main program
  lzfg_exampletop.fugr.abap          # its includes
  z_example_read.fugr.abap           # and its function modules

--flat puts every file in a single src/ folder instead, which is easier to grep and keeps paths short. Both layouts hold the same files, and the choice is recorded in the manifest — a later bare abap pull refresh keeps whatever the workspace was pulled with, so an existing workspace never reshuffles underneath you.

Switching layout on an existing workspace by passing --se80 or --flat explicitly rewrites the manifest and writes the files at their new paths, but does not remove the files at the old ones. Delete the folder and pull again for a clean switch.

One object, several files

An object is not always one file. A class keeps its local definitions, local implementations, macros and test classes in separate ADT includes, and SAP edits, locks and transports each of them on its own. Pull brings down all five:

FileADT include
zcl_x.clas.abapthe class itself
zcl_x.clas.locals_def.abaplocal type and class definitions
zcl_x.clas.locals_imp.abaplocal class implementations
zcl_x.clas.macros.abapmacros
zcl_x.clas.testclasses.abapABAP Unit test classes

Includes the class never used are skipped rather than written as empty files: SAP answers 404 for one that was never created, and returns nothing but a commented banner for one that exists and is empty. Neither becomes a file, so a class with no local helpers still arrives as a single .clas.abap.

Push works the same way in reverse. Editing only zcl_x.clas.testclasses.abap writes only that include, so the transport records the test include and nothing else. A class edited across three files is still activated once.

Function groups are expanded on pull. A package listing only names the group, so the CLI resolves its includes and function modules through the same repository node structure Eclipse uses — without that step a function module could never be pulled or pushed at all. They are filed under the group, the way SE80 shows them.

Local objects: $TMP

Local packages work like any other, with one wrinkle: $TMP is a single package shared by every developer on the system. On a busy system that is tens of thousands of objects, most of them somebody else's.

So a $ package defaults to your objects — the user the session is authenticated as:

$ abap pull '$TMP' --dest ~/Documents/my-local
connected to dev-100, listing $TMP owned by DEVELOPER...
pulled $TMP from dev-100 - 69 objects in 84 files, 273,395 bytes in 5.8s

Quote it. $TMP is a variable reference to your shell, and unquoted it expands to nothing before the CLI ever sees it:

abap pull '$TMP'      # correct
abap pull "\$TMP"     # also fine
abap pull $TMP        # wrong - zsh/bash expand this to an empty string

To read somebody else's local objects, or everyone's:

abap pull '$TMP' --user ANOTHER_DEV --dest ~/Documents/ANOTHER_DEV
abap pull '$TMP' --user '*' --match 'ZCL_A*'     # everyone, narrowed by name

The default SE80 tree is the same grouping Eclipse shows under a user's $TMP, which makes a folder per developer easy to browse side by side:

abap pull '$TMP' --dest ~/Documents/DEVELOPER
abap pull '$TMP' --user ANOTHER_DEV --dest ~/Documents/ANOTHER_DEV
DEVELOPER/
  Business Services/{Service Bindings,Service Definitions}   4
  Class Library/{Classes,Interfaces}                        22
  Core Data Services/{Data Definitions,Behavior,...}        15
  Dictionary/{Database Tables,Views}                         4
  Enhancements/                                              1
  Function Groups/                                           1
  Message Classes/                                           1
  Programs/                                                 21

Counts are objects; each is a folder of one or more files.

Pushing back to $TMP needs no transport — local packages are never transported. See Guard rails.

Pulling part of a package

--match filters by object name on the server, --type by ADT object type. They combine, and either one alone is enough:

abap pull ZEXAMPLE_PKG --type CLAS,INTF        # just the ABAP OO objects
abap pull ZEXAMPLE_PKG --match 'ZCDS_I_*'      # just those CDS views
abap pull ZEXAMPLE_PKG --match 'ZCL_EX*' --type CLAS

--type accepts either the short form (CLAS, DDLS) or the full ADT code (CLAS/OC, DDLS/DF). If nothing matches, the pull stops with nothing in <PACKAGE> matched rather than creating an empty workspace.

Filters are recorded in the manifest, so a later bare abap pull refreshes the same narrow set instead of silently widening to the whole package. Override them by passing the options again — --match '*' and --user '*' widen back out.

Pushing

Push only ever sends files whose hash differs from the manifest baseline, so an edit to one method sends one object. It runs a series of checks in order, and the first five happen before any network call:

  1. Manifest must exist — otherwise there is no baseline to diff against.
  2. Deleted files are ignored, with a note. Push never deletes objects in SAP.
  3. Nothing modified — it exits saying so.
  4. Non-source objects are refused. A modified .msag.xml or .srvb.xml aborts the whole push rather than pushing the rest silently.
  5. A transport is required for transportable packages. See Guard rails.
  6. Drift check — one parallel read per modified file, comparing the server copy of that exact include against the baseline. Any conflict aborts before a single byte is written.
  7. Write loop — for each file, serially: LOCK the object, PUT that include with corrNr, UNLOCK. The local baseline is re-hashed and the manifest saved after each file, so a failure halfway through does not make already-pushed work look unpushed. Re-running pushes only what is left.
VariationWhat it does
abap pushrefused on a transportable package — a transport is never invented for you
abap push --transport DEVK900123the normal case: checks drift, then pushes every locally modified object under that TR
abap push --dry-runlists exactly what would be sent and stops. Fully offline — it makes no ADT calls at all
abap push --forceskips the drift check and overwrites whatever is on the server. Use only when you know your copy should win
abap push --dest <path>pushes the workspace at that path instead of the current directory
abap push --activateactivates the objects it just pushed, in one run afterwards
abap push --system <name>only accepted when <name> is the system the workspace was pulled from; anything else is refused
abap push --traceprints every lock, PUT and unlock with full payloads

Pushed objects stay inactive until something activates them. Add --activate to do it in the same run — see Activating.

New objects

A file that the manifest has never seen is a new object. Drop it anywhere in the workspace with the right name and extension — the folder it sits in does not matter, only the file name does — and push creates it in SAP first, then writes the source into it, the same two steps Eclipse performs when you add a class:

$ abap status
  A  src/zce_new_view.ddls.asddls  (new, not in SAP yet)
  A  src/zcl_new_thing.clas.abap  (new, not in SAP yet)

0 modified, 2 new, 0 deleted

$ abap push --transport DEVK900201 --activate
  A  src/zce_new_view.ddls.asddls  (new)
  A  src/zcl_new_thing.clas.abap  (new)
  created ZCE_NEW_VIEW
  created ZCL_NEW_THING

activating 2 object(s)...

2 object(s) pushed
activated 2 object(s) on dev-100

The object name comes from the filename and the type from the extension, so zcl_thing.clas.abap becomes class ZCL_THING. Part suffixes are understood too: zcl_thing.clas.testclasses.abap is the test include of ZCL_THING, not an object called ZCL_THING_CLAS_TESTCLASSES. The object lands in the workspace's package, under the transport push resolved. Descriptions are taken from @EndUserText.label where the source has one, and default to the object name otherwise.

Creatable types are classes, interfaces, programs, CDS data definitions, metadata extensions, access controls and service definitions. A new file of any other type is refused by name rather than ignored — create it in Eclipse/ADT once, then re-pull.

New function group objects are the exception worth knowing: four ADT types share the .fugr.abap extension, so a new file with that extension is read as a function group. A new function module cannot be created by dropping a file — create it in Eclipse/ADT, then re-pull and edit it here.

Files in dot-directories are never considered, so .git and .adt stay out of it. Deleting a file still does nothing: push never deletes objects in SAP.

Activating

A push leaves inactive versions behind, exactly as editing in Eclipse does. --activate finishes the job in the same run:

$ abap push --activate
  M  src/zcl_example_utils.clas.abap
using DEVK900201 (DEVELOPER, 'Example work') - it already holds these objects
  recording under your task DEVK900202
  pushed ZCL_EXAMPLE_UTILS

activating 1 object(s)...

1 object(s) pushed
activated 1 object(s) on dev-100

It activates only the objects that were just pushed, never the whole workspace — activating a package you did not touch is a good way to turn a colleague's half-finished object into an active one. There is deliberately no abap activate command for that reason.

Everything pushed goes into a single activation run, which is what you want after a mass change: SAP resolves the order itself, so a CDS view and the class that selects from it activate together rather than failing on sequence. Activation only starts once every write has completed and unlocked, because SAP refuses to activate a locked object.

Errors are reported per object, with the line number where SAP has one, and the command exits non-zero so a script stops:

$ abap push --activate
  pushed ZCL_EXAMPLE_UTILS

activating 1 object(s)...
  !  CLAS ZCL_EXAMPLE_UTILS line 42: Field "LV_MISSING" is unknown.
error 1 activation error(s) - the objects stay inactive until they are fixed

The source is still in SAP and still in the transport at that point; only the active version is unchanged. Fix the file locally and push again. Warnings are printed but do not fail the run.

Deleting

Deletion is the one irreversible thing here, so it is deliberately awkward: it takes object names rather than file paths, only accepts objects the workspace already tracks, and asks before it acts.

$ abap delete ZCL_EXAMPLE_DEMO
  D  ZCL_EXAMPLE_DEMO  (src/zcl_example_demo.clas.abap)

This deletes 1 object(s) from dev-100. Deleted objects cannot be restored by this CLI.
Continue? [y/N]: y
using DEVK900201 (DEVELOPER, 'Example work') - it already holds these objects
  recording under your task DEVK900202
  deleted ZCL_EXAMPLE_DEMO

1 object(s) deleted

--dry-run lists and stops; --yes skips the prompt for scripts. The same transport rules as push apply, because a deletion has to be recorded somewhere before it can reach the next system — see Guard rails. On success the local file and its manifest entry go too, so status stays clean.

A name the workspace does not track is refused rather than guessed at:

$ abap delete ZCL_NOT_HERE
  !  ZCL_NOT_HERE is not in this workspace
error only objects tracked by the workspace can be deleted - re-pull first

Transports

abap transports is SE09 without the GUI, and abap transport works on a single request.

Listing what you own
$ abap transports --unreleased
workbench / modifiable
  DEVK900101  ABC-1234 example read buffer
      task DEVK900102  DEVELOPER

customizing / modifiable
  DEVK900105  ABC-5678 example configuration
      task DEVK900106  DEVELOPER

2 request(s)

Filters come in pairs, and passing neither of a pair — or both — means no narrowing at all:

OptionEffect
--released / --unreleasedonly released, or only what you can still change
--workbench / --customizingonly that category
--user / -usomebody else's requests; '*' is everybody
--objectsalso list the objects inside each request
--system / -sa system other than the workspace's

Status is filtered by SAP, category by the CLI. That split is not arbitrary: the tree endpoint defaults requestStatus to released, so asking for everything is genuinely two queries, while the section a request is filed under is what the tree is authoritative about — a request whose tm:type disagrees with its folder is still in that folder.

Creating one
$ abap transports new 'ABC-1234 example read buffer'
created DEVK900101  ABC-1234 example read buffer
  task DEVK900102  DEVELOPER
abap push --transport DEVK900101

--customizing makes a type W request instead of type K. The transport target is left to SAP unless --target says otherwise, so the transport layer of the package decides where the request goes rather than a guess made here.

The task inside it is classified as Development/Correction (or Customizing) as a second step, because SAP creates every task through this API as Unclassified whatever the create payload asks for, and an unclassified task refuses every object with Changes to objects are only allowed in correction/repair.

A request created this way is a transportable one. Objects in a local package can only live in a local request — SAP says Only edit objects from package X in local requests — and those are the ones SAP generates for you. There is no way to ask for a local request here yet, so for a local package, reuse the generated request instead of making a new one.

Descriptions are checked against SAP's 60-character limit before anything is sent, because a request created with a truncated description has to be deleted and remade.

Attributes

CTS attributes are the Jira keys and release markers your system defines on a request. attr shows them, and sets them:

$ abap transports attr DEVK900103 Z_TICKET=ABC-1234
  +  Z_TICKET (Ticket reference) = ABC-1234

  Z_FEATURE (Feature reference) = ABC-5678
  Z_TICKET (Ticket reference) = ABC-1234

2 attribute(s) on DEVK900103

--attr / -a sets them at creation time, repeat it per attribute:

$ abap transports new 'ABC-1234 fix' -a Z_TICKET=ABC-1234 -a Z_FEATURE=ABC-5678
created DEVK900103  ABC-1234 fix
  task DEVK900104  DEVELOPER
  +  Z_TICKET (Ticket reference) = ABC-1234
  +  Z_FEATURE (Feature reference) = ABC-5678

That is create-then-set, not one call: SAP has no way to carry attributes on the request that creates it, and sets them one at a time.

NAME=VALUE splits on the first =, so a value may contain more of them. Setting a name the request already carries replaces its value rather than adding a second copy — SAP has two different actions for those cases (addattribute and modifyattribute), and the second addresses the attribute by position, not by name. Positions shift as attributes come and go, so the current list is read immediately before every write.

abap transports attr --names lists what your system defines:

$ abap transports attr --names
  Z_DEPLOYMENT  Deployment reference
  Z_FEATURE  Feature reference
  Z_TICKET  Ticket reference

46 attribute(s) defined on this system

That list is SAP's own value help, which is what the Eclipse dialog uses, so it is the set you are allowed to set. The underlying table WBOATTR holds a few more — SAPCORR, SAPIMG, TAKT* and friends — that SAP maintains itself and hides from the dialog. Anything the value help omits is refused by SAP rather than silently ignored:

$ abap transports attr DEVK900103 Z_NOT_A_THING=x
error Z_NOT_A_THING is not a valid attribute; enter a valid attribute
Deleting a request
$ abap transports delete DEVK900103
DEVK900103
  task DEVK900104  DEVELOPER  Modifiable
  D  R3TR CLAS ZCL_THING

This deletes DEVK900103 and the 1 object(s) in it. A deleted request cannot be
restored by this CLI.
Continue? [y/N]:

The contents are read and shown before the prompt, so you can see what goes with it. --yes / -y skips the confirmation for scripts.

SAP does the refusing here rather than the CLI: a released request cannot be deleted, and neither can one still holding object locks. The transport number is checked for shape before anything connects, so a typo costs nothing.

Working on one request

abap transport inspects a request, and adds or removes objects in it:

$ abap transport DEVK900201 list
task DEVK900202  DEVELOPER  Modifiable
  locked  R3TR CLAS ZCL_EXAMPLE_READER
  locked  R3TR DDLS ZCE_EXAMPLE

2 object(s) in DEVK900201

$ abap transport DEVK900301 add ZCL_EXAMPLE_UTILS
  +  R3TR CLAS ZCL_EXAMPLE_UTILS

1 object(s) in DEVK900301

Objects live in tasks, not requests, so a request number is resolved to the task you own before anything is written.

Naming what to add

Three forms, because not every entry is a whole object:

FormRecordsExample
NAMER3TR + the type from the workspace manifestZCL_EXAMPLE_UTILS
CLASS=>METHODLIMU METH — one method on its ownZCL_EXAMPLE_UTILS=>GET_OFFSET
PGMID:TYPE:NAMEexactly what you typeR3TR:TABL:ZMY_TABLE

The method form is the one that matters for shared classes, and produces the same entry SAP writes when you edit a single method in Eclipse:

$ abap transport DEVK900302 add 'ZCL_EXAMPLE_UTILS=>GET_OFFSET'
  +  LIMU METH ZCL_EXAMPLE_UTILS             GET_OFFSET

1 object(s) in DEVK900302

That spacing is not cosmetic: SAP stores a LIMU METH key as the class name padded to 30 characters followed by the method, so the CLI builds it that way. Interface implementations work through the same form — ZCL_THING=>ZIF_THING~DO_IT.

Quote the argument. => means nothing to zsh or bash, but shells differ and a stray > would redirect to a file.

The plain NAME form needs the object in the workspace, since that is where its type comes from. PGMID:TYPE:NAME is the escape hatch when it is not, or when you want an entry the CLI would never infer. Types for remove come from the request itself, so an entry can still be named after its object is gone.

Removing works, and is addressed by position. SAP identifies an entry by its position in the request, not by name, so the current contents are read immediately before the write — positions shift as entries go:

$ abap transport DEVK900201 remove ZCL_NEW_THING ZCE_NEW_VIEW
  -  R3TR CLAS ZCL_NEW_THING removed
  -  R3TR DDLS ZCE_NEW_VIEW removed

2 object(s) removed from DEVK900201

The result is read back afterwards rather than trusting the 200: omit the position and SAP answers 200 and does nothing at all, so anything still in the request is reported as a failure instead of a success.

Removing an object's only entry releases its lock, which is how you hand a class back without releasing the whole request.

Calling an OData service

abap api sends a request to any OData service the system publishes, over the session you are already authenticated on. No separate logon, no second set of credentials, and --trace shows the exchange like every other request.

$ abap api GET ZSD_EXAMPLE_API Items -n ZSB_EXAMPLE_API -q '$top=2'
{
  "@odata.context": "$metadata#Items",
  "value": [
    { "name": "FIRST", "type": "P", "low": "20210907" },
    { "name": "SECOND", "type": "P", "low": "20240101" }
  ]
}

200 - 2 row(s)
Argument
METHODGET (default), POST, PUT, PATCH, DELETE, HEAD
SERVICEthe service definition, e.g. ZSD_EXAMPLE_API
ENTITYentity set or path, e.g. Items or Items('X')/Details
OptionEffect
--namespace / -nthe service binding for a v4 service, usually ZSB_*
--v2a Gateway service rather than a RAP one
--query / -qone query option, e.g. '$top=10'. Repeatable
--body / -bJSON for a write, inline or @file.json
--metadatafetch $metadata instead of calling an entity
--rawprint the response verbatim instead of pretty-printing

Every operation is one command, and the shape is always the same — method, service, entity:

OperationCommand
read a collectionabap api GET SRV Items -q '$top=10'
read one entityabap api GET SRV "Items('ABC')"
read the contractabap api GET SRV --metadata
createabap api POST SRV Items -b '{"name":"X"}'
update some fieldsabap api PATCH SRV "Items('ABC')" -b '{"low":"1"}'
replace an entityabap api PUT SRV "Items('ABC')" -b @full.json
deleteabap api DELETE SRV "Items('ABC')"
Which name goes where

This is the part that catches people out. A v4 service is published under its binding, not its definition, and the two are different objects:

/sap/opu/odata4/sap/<binding>/srvd_a2x/sap/<service>/0001/<entity>

So -n takes the SRVB/SVB object and the positional argument takes the SRVD/SRV one. When they have the same name, -n can be left out. A v2 service has no binding and sits at a flatter path, so --v2 plus the service name is all it needs:

/sap/opu/odata/sap/<SERVICE>/<entity>

abap transports will not help you find these — use abap pull on the package, or Eclipse, to see which SRVB/SVB belongs to which SRVD/SRV.

Reading the service first

--metadata returns the EDMX document, which is the authoritative list of entity sets and their fields — worth checking before guessing a filter:

abap api GET ZSD_EXAMPLE_API --metadata -n ZSB_EXAMPLE_API
abap api GET ZSD_EXAMPLE_API --metadata -n ZSB_EXAMPLE_API > service.xml

--metadata and --raw print the payload byte for byte, with no wrapping and no colour, so redirecting either into a file gives you a document that still parses.

Writes

POST, PUT, PATCH and DELETE fetch a CSRF token from the service before sending. That extra round trip is not optional: Gateway issues tokens per service and rejects the one ADT handed out at logon.

Create. The body is JSON, inline or @file:

$ abap api POST ZSD_EXAMPLE_API Items -b '{"name":"Z_MY_VAR","type":"S","opti":"EQ","low":"ALPHA"}'
{
  "name": "Z_MY_VAR", "type": "S", "numb": "0",
  "sign": "I", "opti": "EQ", "low": "ALPHA", "high": ""
}
201

A managed RAP service often fills keys for you — read them back out of the response rather than assuming what was assigned.

Update one entity, addressed by its key predicate. Send only the fields that change; the rest are preserved:

abap api PATCH ZSD_EXAMPLE_API "Items(name='Z_MY_VAR',type='S',numb='1')" -b '{"low":"1500","high":"2500"}'
abap api PATCH ZSD_EXAMPLE_API "Items(name='Z_MY_VAR',type='S',numb='1')" -b @change.json

Delete answers 204 with no body:

$ abap api DELETE ZSD_EXAMPLE_API "Items(name='Z_MY_VAR',type='S',numb='1')"
204 no content

A body on a GET is refused before anything is sent, as is a malformed one — -b is parsed locally first, so a typo costs no round trip.

Key predicates

Anything after the entity set is passed through untouched, so the full OData addressing syntax works. Quote the whole argument — the parentheses and single quotes are shell metacharacters:

GoalENTITY argument
a collectionItems
one entity, single key"Items('ABC')"
one entity, compound key"Items(name='Z_VAR',type='S',numb='1')"
a navigation property"Items('ABC')/_Details"

A key value SAP returns unpadded ("1") must be used unpadded in the predicate, even where the field is NUMC and you supplied "0001" on create.

Query recipes

-q is repeatable and each option is passed verbatim, so anything the service supports works. Escape $ in double quotes, or use single quotes, so the shell does not expand it:

# filter, sort, project, page
abap api GET ZSD_EXAMPLE_API Items -q "\$filter=name eq 'Z_VAR' and type eq 'S'" \
                                   -q '$orderby=numb' -q '$select=numb,low,high' -q '$top=20'

# highest counter: sort descending and take one
abap api GET ZSD_EXAMPLE_API Items -q "\$filter=name eq 'Z_VAR'" -q '$orderby=numb desc' -q '$top=1'

# prefix search
abap api GET ZSD_EXAMPLE_API Items -q "\$filter=startswith(name,'Z_MY')"

# every navigation property in one call
abap api GET ZSD_EXAMPLE_API Items -q "\$filter=Product eq 'ABC'" -q '$expand=*' -q '$top=20'

# a named expand, and paging past a row the service chokes on
abap api GET ZSD_EXAMPLE_API Items -q '$expand=_Details' -q '$skip=21' -q '$top=20'

$expand=* pulls every navigation property the entity declares — a single-valued one arrives as an object, a collection as an array:

{
  "IntObjNo": "921667",
  "_Appoint": { "term_end_date": "2027-08-15" },
  "_ConfigParams": [ { "ConfigName": "CHARGE_TYPE", "ConfigValue": "Recurring" } ]
}
Piping it somewhere

Only the payload goes to stdout. The status line, row count and the TLS warning all go to stderr, so a pipe or redirect gets a clean document either way:

abap api GET ZSD_EXAMPLE_API Items -q '$top=50' | jq '.value[].name'
abap api GET ZSD_EXAMPLE_API --metadata -n ZSB_EXAMPLE_API > service.xml
abap api GET ZSD_EXAMPLE_API Items --raw > items.json

--raw prints compact JSON instead of the indented, coloured form — use it when a machine is reading, not a person.

Errors

The service's own message is what you see, not the status code. A RAP handler that rejects a call explains why:

$ abap api GET ZSD_EXAMPLE_API Items -n ZSB_EXAMPLE_API
error No filters were provided

$ abap api GET ZSD_EXAMPLE_API Items -n ZSB_EXAMPLE_API -q "\$filter=Nope eq '1'"
error Invalid token 'Nope' at position '1'

Both dialects are unwrapped: v4 carries the text directly, v2 nests it under message.value. Without that you would get HTTP 500 and nothing else.

Not every failure is that clear, because not every service is. A missing entity may come back as Unspecified provider error occurred, and a duplicate key can surface as ABAP Runtime error 'RAISE_SHORTDUMP' — those are the server's words, passed through rather than dressed up. The command exits non-zero in every case, so a script stops.

Object types

abap pulls exactly what it can push back. A type earns its place by having something ADT will accept a write for; anything else is skipped rather than downloaded as read-only metadata you would only discover was read-only when the push refused it.

To see the current list on your own install:

abap types          # types that are pulled and pushed
abap types --all    # plus the ones deliberately skipped
Class Library/Classes
  CLAS/OC   .clas.abap, .clas.locals_def.abap, .clas.locals_imp.abap,
            .clas.macros.abap, .clas.testclasses.abap

Dictionary/Data Elements
  DTEL/DE   .dtel.xml  xml

Function Groups
  FUGR/F    .fugr.abap
  FUGR/FF   .fugr.abap
  FUGR/I    .fugr.abap

Two shapes are covered.

Source objects are plain text at a source endpoint: classes (with their local definitions, local implementations, macros and test classes), interfaces, programs, includes, function groups, function modules and their includes, type groups, transformations, CDS data definitions, metadata extensions, annotation definitions, access controls, behavior definitions, service definitions, database tables and structures.

Dictionary, text and service objects have no source endpoint at all — they are edited as the object's own XML document: data elements, domains, table types, lock objects, message classes and service bindings. A message class carries every message number and text, and a service binding carries its contract and published state, so those are editable too. SAP stamps changedAt when it stores one, so push re-reads the object afterwards and takes the server's copy as the new baseline; without that every pushed object would look changed for ever after.

Skipped, because ADT answers with a SAP GUI shim rather than a real resource: DDIC views and BSP applications. Enhancement implementations are skipped as well — they do have a resource, but SAP fails to serialise a good share of them with a 500. So are packages and authorization defaults, types the registry has never heard of — SEGW projects, SICF nodes, transactions — and objects another SAP service owns, such as enterprise service proxies. All of them are counted in one line rather than producing a 404 each:

pulled ZEXAMPLE_SUITE from dev-100 - 3894 objects in 3974 files in 97.6s
1664 object(s) have no editable content in ADT and were skipped

Objects that fail for any other reason are reported one per line, and the rest of the pull still completes.

Adding a type is one line in objects.py — a Part names the file suffix and the URI segment that serves it. An empty segment means the object resource itself, which is how the XML-backed types are written.

Guard rails

A transport is required. Pushing to a transportable package without --transport is refused, rather than letting SAP quietly generate a Generated Request for Change Recording you never asked for. Local $ packages ($TMP and friends) are never transported, so they push without one.

$ abap push
error package ZEXAMPLE_PKG is transportable - pass --transport <TR>
      (only local $ packages can push without one)

Push goes back where it came from. A workspace is bound to the system it was pulled from. The baseline hashes and object URIs in the manifest describe that system and no other, so a diff against anything else is meaningless. --system may name that system, but it cannot redirect the push:

$ abap push --system qa-300 --transport DEVK900123
error workspace was pulled from dev-100, refusing to push to qa-300 -
      pull the package from qa-300 into its own folder if that is the real target

Moving code between systems is what the transport route is for, not a re-pointed push.

The transport is checked before anything is written. SAP locks an object in one request; tasks are just per-developer slots inside it. So before the first write, push asks SAP which request already holds each object.

When exactly one request holds them, there is nothing to choose — SAP would reject any other — so push adopts it and says so:

$ abap push
  M  src/zcl_example_utils.clas.abap
using DEVK900456 (DEVELOPER, 'Example work') - it already holds these objects
  recording under your task DEVK900457
  pushed ZCL_EXAMPLE_UTILS

1 object(s) pushed

If the request is open but you have no task in it — developer A holds the object under their task — SAP opens one for you, and push reports that instead:

using DEVK900456 (ANOTHER_DEV, 'Feature work') - it already holds these objects
  SAP will open a task for DEVELOPER in it

--transport is still honoured, and still checked. Pass one that does not lead to the holding request and push stops before writing:

$ abap push --transport DEVK900123
  !  src/zcl_example_utils.clas.abap is locked in DEVK900456 (DEVELOPER, 'Example work')
error 1 object(s) already locked in DEVK900456, not DEVK900123 - SAP locks an
      object in one request only, so re-run with --transport DEVK900456

The request number and any task number inside it are all accepted, since they all record into the same request.

When a transport really is needed, you get the plain error — no request holds these objects yet, so there is nothing to infer and nothing is invented for you:

$ abap push
error package ZEXAMPLE_PKG is transportable - pass --transport <TR>
      (only local $ packages can push without one)

Mixed pushes are handled the obvious way. If two of three objects sit in DEVK900456 and the third is in no request at all, the third joins the same request and is called out by name rather than slipped in quietly:

  +  src/zcl_new_helper.clas.abap is in no request yet, it will be added to DEVK900456

Objects spanning several requests are refused, with the request numbers listed, because one push cannot legally record into both. Local $ packages skip the check entirely: they are never transported.

Locks are taken and released for you. There is no separate lock step, and no unlock to remember. Each object is locked immediately before its write and unlocked immediately after, which is where the transport entry comes from. If somebody else is holding the object, SAP refuses the lock and push stops with the server's own message, having written nothing to that object:

$ abap push --transport DEVK900123
error User ANOTHER_DEV is currently editing ZCL_EXAMPLE_UTILS

Locks are session-bound, so they cannot leak: the unlock runs even when the write fails, and the session is returned to stateless afterwards, which releases any enqueue SAP still holds. A push interrupted halfway leaves earlier objects written and re-baselined, so re-running sends only what is left.

Concurrent edits are detected. The manifest records the hash of exactly what the server handed you at pull time, so re-fetching and re-hashing shows whether anyone touched the object since — SE80, Eclipse, or another push. Status marks three cases:

MarkerMeaning
Myou changed it
Aa new file, not in SAP yet
Rsomebody else changed it on the server, you did not
Cconflict — changed in both places
$ abap status --remote
  C  src/zcl_example_utils.clas.abap  (also changed on server)

1 modified, 0 deleted, 1 changed on server
1 conflict(s) - push will refuse these until you re-pull

Push performs the same check and stops before writing anything:

$ abap push --transport DEVK900123
  C  src/zcl_example_utils.clas.abap also changed on dev-100 since your pull
error 1 object(s) changed on the server - pushing would overwrite that work.
      Re-pull to inspect, or use --force to overwrite.

Resolve it by re-pulling and reapplying your edit, or override with --force if you know your copy should win. --dry-run shows what would be sent without sending it.

This costs one read round trip per modified object before the write; --force skips the check.

Developer Workflow

┌──────────────┐
│     SAP      │
└──────┬───────┘
       │
       │  abap pull
       ▼
┌──────────────────────┐
│  Local ABAP Files    │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│       VS Code        │
│                      │
│  Copilot / AI can:   │
│  • inspect code      │
│  • make changes      │
│  • refactor          │
│  • review changes    │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│   Developer Review   │
│                      │
│  abap diff           │
│  abap status         │
│  approve changes     │
└─────────┬────────────┘
          │
          ▼
     Push to SAP
          │
     ┌────┴───────────────┐
     │                    │
     ▼                    ▼
┌──────────────┐   ┌────────────────────┐
│   abap cli   │   │ SAP MCP / ADT /    │
│              │   │ Existing SAP Tools │
│ Source code  │   │                    │
│ pull / push  │   │ Operations not yet │
│ status/diff  │   │ supported by CLI   │
│ activate     │   │                    │
└──────┬───────┘   └─────────┬──────────┘
       │                     │
       └──────────┬──────────┘
                  │
                  ▼
             ┌─────────┐
             │   SAP   │
             └─────────┘
Reviewing before you push

Two commands, two different questions.

abap status tells you which objects you changed. It hashes every file against the manifest baseline recorded at pull time, so it needs no network and no git repository.

abap diff shows what changed, line by line, against the copy currently in SAP — and, from the manifest baseline, who changed it:

Console samples from here on use short src/… paths, the --flat layout, so lines fit. The default SE80 tree prints the same files at longer paths.

$ abap diff
'-' is dev-100 as it stands now, '+' is your local copy - what push would make it
trailing spaces shown as ·, tabs as →

  R  src/zcds_i_example.ddls.asddls  (changed on dev-100, not by you)
  line 8
   as select from zdt_example

 {
-  key object_id,  //changes
+  key object_id,
   key number_int,
  line 18
       cycle_start_date,
-      cycle_end_date··
+      cycle_end_date
 }

1 object(s) differ from dev-100
1 of them changed on dev-100 since your pull - pushing would overwrite that work, re-pull instead

Two readability details, because an ABAP diff is often whitespace: changed lines show trailing spaces as · and tabs as →, so an invisible edit is visible, and a file whose only difference is spacing is flagged whitespace only. Hunk positions are printed as plain line 18 rather than @@ -18,5 +18,5 @@.

The marker is the important part, because a diff on its own cannot tell you which side moved:

MarkerMeaning
Myou changed it; + is your edit, ready to push
Rsomebody changed it in SAP and you did not; - is their work, and pushing would wipe it
Cboth sides changed it

In the example above someone edited the view in SAP GUI. The - line is their change and the + line is your untouched copy, so pushing would silently revert them — hence the warning. Re-pull instead.

The manifest stores hashes, not text, so diff fetches the server copy. That is the same request status --remote makes, kept rather than discarded, and costs one GET per tracked object.

Git is still worth adding on top if you want local history and revert:

cd ~/Documents/example-pkg
git init && git add -A && git commit -m "baseline: pulled from dev-100"

That buys git diff offline, plus git checkout -- <file> to throw a change away. Entirely optional — abap diff, abap status and abap push --dry-run cover review on their own.

What still needs SAP tooling

abap covers the develop-review-ship loop for the object types in Object types. A few things deliberately live elsewhere, because they are one-off setup steps rather than part of the edit cycle:

TaskWhere
release a transportSE09/SE10
object types outside the registryEclipse/ADT or SAP GUI
SEGW projects, SICF nodes, DDIC viewsSAP GUI — no ADT editor exists
TVARVC entries, client copiesSAP GUI or an MCP server

Everything else — pulling, editing, diffing, pushing, activating, adding to and removing from a transport, deleting objects, calling an OData service — is abap.

Run abap types to see exactly which object types the installed build handles; it reads the same registry the pull and push use, so it cannot drift from the code.

What you get

Correct transport entries. Editing one method records exactly one LIMU METH entry, the same as Eclipse. The class is not locked as a whole, so a colleague can work on a different method in a different transport, and importing your transport will not revert theirs.

DEVK900303   LIMU   METH   ZCL_EXAMPLE_UTILS             GET_OFFSET

The whole object, not just its main source. A class comes down as its own source plus its local definitions, local implementations, macros and test classes, each of them a file you can edit and push on its own. Function groups are expanded into their includes and function modules. See One object, several files.

No silent overwrites. Push refuses to clobber work that appeared on the server after your pull, and refuses to auto-generate a transport behind your back. See Guard rails.

Speed. Enumeration is a single request; every source fetch runs in parallel. A 30-object package lands in about 2.3 seconds.

Nothing installed in SAP. ADT is already there.

Systems and credentials

A system is a named connection profile: host, SAP user, client. Add one interactively — init prompts for anything you leave out:

$ abap init
System name (e.g. dev-100): dev-100
Host URL (https://host:port): https://sap.example.com:44300
SAP user: DEVELOPER
SAP client: 100
saved dev-100 to /Users/you/.abap-adt/config.json
next: abap login --system dev-100

Or non-interactively, which is what you want in a script:

abap init --name dev-100 \
          --host https://sap.example.com:44300 \
          --user DEVELOPER \
          --client 100 \
          --description "Dev, client 100"

Add --insecure to skip TLS verification for systems with a self-signed certificate. Every init makes that system the default.

Then store the password once. It goes to the OS keychain under service abap-adt-cli, account <system>:<user> — never to a file, never to the config:

abap login --system dev-100     # prompts, input hidden
abap ping   --system dev-100    # confirm it works
abap systems                    # list them all, '*' marks the default
abap logout --system dev-100    # remove the stored password

Omit --system on any of these when you only have one system, or when the one you want is already the default.

Where everything is stored

Two separate places, on every platform: the profile is a plain file, the password never is.

Connection profilesPassword
macOS~/.abap-adt/config.jsonKeychain (see it in Keychain Access)
Linux~/.abap-adt/config.jsonSecret Service — GNOME Keyring, KWallet
Windows%USERPROFILE%\.abap-adt\config.jsonCredential Manager, under Windows Credentials

The keychain entry is stored under service abap-adt-cli with account <system>:<user>, so several systems and several users coexist without clashing. Nothing readable is written to disk by the CLI.

On a headless Linux box with no Secret Service running, abap login reports no usable keychain backend on this machine. That is expected — use $ABAP_PASSWORD instead, as below.

Changing a password

Run login again. It overwrites whatever was stored for that system:

abap login --system dev-100     # after a SAP password change
abap ping  --system dev-100     # confirm the new one works

This is also the fix when a push suddenly fails with authentication failed after a periodic SAP password expiry. abap logout --system dev-100 deletes the entry outright, and the next command falls back to prompting.

Editing or removing a system

Re-running init with an existing name overwrites that profile and makes it the default again — that is how you move a system to a new host or client:

abap init --name dev-100 --host https://newhost:44300 --user DEVELOPER --client 100

There is no remove command yet. Delete the entry from config.json by hand, and fix default_system if it pointed at what you removed. Same for switching the default without re-running init.

If you change the user of an existing system, the old password stays behind in the keychain under the previous <system>:<user> account. Run abap logout --system <name> before the re-init to clean it up.

The config file

Profiles live in ~/.abap-adt/config.json, created with 0600 permissions. It is plain JSON and safe to edit or check into a dotfiles repo — it holds no secrets:

{
  "default_system": "dev-100",
  "systems": {
    "dev-100": {
      "host": "https://sap.example.com:44300",
      "user": "DEVELOPER",
      "client": "100",
      "insecure": false,
      "description": "Dev, client 100"
    }
  }
}
Which system a command uses

First match wins:

  1. --system / -s on the command
  2. for status, push and a refreshing pull, the system recorded in the workspace manifest at pull time
  3. $ADT_SYSTEM
  4. default_system in the config file
  5. the only configured system, if there is exactly one

With several systems and no default, commands stop and list the names rather than guessing.

push is the exception: it always goes back to the system in the manifest, and --system can only confirm that name, never redirect. See Guard rails.

Passwords and CI

Password lookup is $ABAP_PASSWORD, then the OS keychain, then an interactive prompt. On a build agent with no keychain, set the environment variable and skip abap login entirely. $ADT_HOST, $ADT_USER and $ADT_CLIENT likewise override the profile, so a system need not be configured at all:

export ADT_HOST=https://sap.example.com:44300
export ADT_USER=DEVELOPER
export ADT_CLIENT=100
export ABAP_PASSWORD=...        # from your CI secret store
abap pull ZEXAMPLE_PKG

How it works

abap pull   →  POST /repository/informationsystem/virtualfolders/contents   (1 per package)
            →  POST /repository/nodestructure                   (sub-packages, function groups)
            →  GET  <object><part>                              (parallel, one per part)

abap diff   →  GET  <object><part>                       (parallel, text kept)

abap push   →  GET  <object><part>                       (drift check, parallel)
            →  POST /cts/transportchecks                 (which request owns it)
            →  POST <object>?_action=LOCK                (stateful, serialised)
            →  PUT  <object><part>?lockHandle=...&corrNr=...
            →  POST <object>?_action=UNLOCK
            →  POST /activation                          (--activate, one batch)

<part> is /source/main for most objects, and one of /includes/definitions, /includes/implementations, /includes/macros or /includes/testclasses for the other texts a class owns. The lock is always taken on the object; only the PUT targets an individual include.

Reads are stateless so they parallelise. Locks are session-bound, so writes are serialised and the session always returns to stateless afterwards, releasing any server-side enqueue. Activation comes last and in a single call, because SAP refuses to activate a locked object and resolves dependency order itself — and an object edited across several of its includes is activated once, not once per file.

Transport granularity is not something this CLI implements — it falls out of using the same API Eclipse does. SAP decides which includes actually changed and records those.

ADT serves CRLF; files are stored with LF locally so a pull/push round trip does not make every object look modified.

Versions and updating

abap version reports what is actually running, and how it got there:

$ abap version
abap-adt-cli 1.5.0 (pipx, /Users/you/Library/Application Support/pipx/venvs/abap-adt-cli)

$ abap version --check
abap-adt-cli 1.5.0 (pipx, ...)
1.5.2 is available - run 'abap update'

The version is read from the installed distribution, not from the source tree, so a checkout sitting next to an older installed copy cannot make it lie.

abap update              # install the newest release, if it is newer
abap update --check      # print the command it would run, change nothing
abap update --version v1.5.2
abap update --force      # reinstall even when nothing is newer

It works out how the CLI was installed and upgrades it the matching way — pipx upgrade for a pipx install, pip install --upgrade for a venv. A source checkout is refused, because git pull is the right answer there:

$ abap update
error this looks like a source checkout, not an installed copy -
      update it with 'git pull' instead

A --version argument is matched against a version pattern before it reaches the install URL. Anything else is refused rather than interpolated.

abap update always follows the public repository, because it reads the GitHub releases API and that is where releases are published.

Cutting a release

There is one version number, in src/adt_cli/__init__.py. pyproject.toml reads it from there through [tool.hatch.version], so the two cannot drift.

# bump __version__ in src/adt_cli/__init__.py, then
git commit -am "release 1.6.4"
git tag v1.6.4
git push origin main --tags     # internal, primary
git push public main --tags     # public mirror

abap update finds the newest GitHub release, and falls back to the newest tag when no release has been published for it.

Project status

Every command has been exercised against a live S/4HANA system, not only unit-tested — pulling a package hierarchy of several thousand objects, creating and deleting objects, method-level transport entries, the transport guard, conflict detection, creating and deleting transport requests with their CTS attributes, and a single activation run resolving a cyclic CDS dependency that would fail if the objects were activated one at a time.

Not yet implemented: where-used, syntax check, unit test runs, releasing a transport, and creating a local request for a local package. For those, reach for ADT/Eclipse or an MCP server. See What still needs SAP tooling.

Known rough edges
  • Switching an existing workspace between --se80 and --flat leaves the files at the old paths on disk. Delete the folder and re-pull. See Layout.
  • A new .fugr.abap file is created as a function group, because four ADT types share that extension. Create new function modules in Eclipse/ADT, then re-pull. See New objects.
  • abap transports new always produces a transportable request, so it cannot be used for objects in a local package. See Creating one.

Licence

MIT

MIT, read

History

Latest

v1.6.4

Notes

Install instructions now cover both repositories: the Cisco-internal primary and the public mirror. Documents that 'abap update' follows the public releases API, and how to stay on the internal mirror with pipx instead.

  1. v1.6.3

    Notes

    Follow-up to 1.5.2, which made the README generic but left the same real names in command --help text and tests. Transport ids, ticket keys, CTS attribute names, package and user names in src/ and tests/ are now the same generic placeholders the README uses.

  2. v1.6.2

    Other 2

    Notes

    'abap api' polish after an end-to-end run against a managed RAP service.

    • stdout now carries only the payload. Status line, row count and 'no content' go to stderr, so 'abap api ... | jq' and '> file.json' work without --raw.
    • README documents every operation: collection and key-predicate reads, create with server-assigned keys, PATCH of selected fields, PUT, DELETE, compound key predicates, $expand=*, paging, piping and the error shapes.
  3. v1.6.1

    Notes

    Fix: 'abap api --metadata' and '--raw' printed through Rich, which wrapped the payload at the terminal width and read '[...]' as markup. Redirecting metadata into a file produced XML that no longer parsed. Raw payloads now go to stdout byte for byte.

  4. v1.6.0

    Other 5

    Notes

    New 'abap api' command: call any OData service the system publishes, over the session ADT already authenticated.

    • v2 Gateway and v4 RAP, any HTTP method, plus --metadata
    • -n/--namespace takes the service binding (ZSB_*), which is what a v4 path is built from
    • -q/--query for repeatable query options, -b/--body for JSON inline or @file
    • Writes fetch a CSRF token from the service first, since Gateway issues those per service
    • The service's own error message is surfaced instead of a bare status code, unwrapped for both v2 and v4
  5. v1.5.2

    Other 3

    Notes

    Documentation.

    • New 'First-time setup' section: a numbered walkthrough of init, login, ping and a first pull, with a table explaining each init prompt and where to find the value.
    • Every example in the README is now generic. Real package, class, transport, system and user names have been replaced with ZEXAMPLE_PKG, ZCL_EXAMPLE_*, DEVK9001xx, dev-100 and DEVELOPER.
    • Project status refreshed: creating and deleting transport requests are implemented; releasing a transport and creating a local request are not.
  6. v1.5.1

    Notes

    Fix: a request created by 'abap transports new' could not hold any objects.

    SAP creates the task as 'Unclassified' no matter what the newrequest payload asks for, and an unclassified task rejects every object with 'Changes to objects are only allowed in correction/repair'. The task is now classified with a follow-up changetasktype call (S for workbench, Q for customizing), and the request is read back after creation because the create reply names no task.

    Known limitation, now documented: objects in a local package can only live in a local request, and this API always produces a transportable one.

  7. v1.5.0

    Other 4

    Notes

    'abap transports delete TR' removes a transport request or a single task.

    • Contents (tasks and objects) are read and shown before the confirmation prompt
    • --yes/-y skips the prompt for scripts
    • The transport number is validated for shape before connecting
    • SAP does the refusing: a released request, or one still holding object locks, cannot be deleted
  8. v1.4.0

    Other 2

    Notes

    Sub-package handling corrected.

    SAP's package facet is already hierarchical: listing a package returns the objects of every package beneath it. A pull has therefore always included sub-packages, and still does by default.

    • --no-subpackages now genuinely restricts to the named package, by listing the sub-packages and subtracting what they account for. Previously it only skipped the walk, which changed nothing.
    • 'abap pull' now prints the object and package count as soon as the listing is done, so the breadth is visible before the download starts.
  9. v1.3.0

    Other 5

    Notes

    CTS attributes on transport requests.

    • 'abap transports attr TR' shows the attributes a request carries
    • 'abap transports attr TR Z_JIRA_US=O2CSM-1234' adds it, or replaces the value if already present
    • 'abap transports new DESC -a NAME=VALUE' sets attributes on a request as it is created (repeatable)
    • 'abap transports attr --names' lists the CTS attributes the system defines
    • Setting an existing attribute uses SAP's modifyattribute action addressed by position, re-read before every write
  10. v1.2.1

    Notes

    'abap pull' now issues 30 parallel requests by default, up from 16. Override with --jobs/-j (1-64).

  11. v1.2.0

    Other 4

    Notes

    New 'abap transports' command.

    • 'abap transports' lists every request you own, grouped into workbench/customizing and modifiable/released
    • Filters: --released/--unreleased, --workbench/--customizing, --user ('*' for everybody), --objects
    • 'abap transports new "description"' creates a request and prints its number; --customizing for a type W request, --target to override the transport target
    • Descriptions are checked against SAP's 60-character limit before sending
  12. v1.1.1

    Notes

    Docs: Quick start and Pulling now state that a pull walks the package hierarchy by default, and document that a workspace pulled before 1.1.0 must be widened once with --subpackages --force.

  13. v1.1.0

    Notes

    Pull now walks the package hierarchy by default, with --no-subpackages to take a single package. The first ADT call fails in 5s instead of hanging when the host is unreachable.

  14. v1.0.0

    Notes

    First stable release. Built entirely on the SAP ADT REST API — nothing is installed in the SAP system.

  15. v0.1.0

    Notes

    Fast ABAP development from the command line, built directly on the SAP ADT REST API. Nothing is installed in the SAP system.

Others providing58

Source control for ABAP
  1. abapGitGit client written in ABAP that runs inside the system
  2. abap-transport-reviewTransport release check gated by GitHub pull request approval
  3. abapGitServerGit server implemented in ABAP for abapGit clients
  4. Tools for abapGitHelper programs around abapGit
  5. ABAP remote filesystemVS Code extension editing ABAP systems over ADT as a filesystem
  6. abapgit-commitlintabapGit user exits checking commit messages against repository rules
  7. trm-serverServer-side component of TRM for importing and exporting transports
  8. abap-file-formatsSAP's specification for serialising ABAP objects to files
Transport management
  1. HammerNative macOS code editor for ABAP and CDS
  2. SAP Cloud Transport ManagementMoves MTA archives and content between SAP BTP accounts
  3. Cloud ALM Link for ADTEclipse plugin linking ABAP transports to SAP Cloud ALM features
  4. EPOD ADT MCP ServerMCP server for ABAP on the ADT session, in Eclipse or standalone
  5. furkancosgun/Transport-Management-ToolUploads, downloads and imports ABAP transport requests as files
  6. furkancosgun/ZWARPAdds a program's dependent objects to a transport request
  7. abapToCSAP GUI transaction for one-click transports of copies
  8. ABAP remote filesystemVS Code extension editing ABAP systems over ADT as a filesystem
  9. ActiveControl Transport form creatorVS Code extension automating ActiveControl transport forms
  10. ZCTS_BROWSERReport creating transports of copies without a new version
  11. qttTool mapping BEx queries to the transport requests carrying them
  12. KlystronMCP server in ABAP inside the application server, with agent skills
  13. ZTCTA transport checking tool that analyses ABAP transports at object level before they move to production, determining a safe order and flagging risks.
Agent access to ABAP code
  1. @abap2ui5/mcp-serverMCP server for the abap2UI5 check, deploy and run loop
  2. arc-1-bopf-enharc-1 extension for reading and maintaining BOPF enhancements
  3. HammerNative macOS code editor for ABAP and CDS
  4. arc-1-lspEdition of ARC-1 that runs ADT calls through adt-ls
  5. arc-1MCP server over the ADT REST API for ABAP objects
  6. arc1-abap-bridgeVS Code bridge between arc-1 and ADT for VS Code
  7. arc1-adt-abap-mcp-extEclipse add-on enabling the ADT MCP server with read-only tools
  8. ABAP Accelerator MCP ServerAWS's ABAP MCP server for Amazon Q Developer and Kiro
  9. Modernization Agents for SAP on AWSAWS agents for ABAP, PI/PO and BW modernisation work
  10. @mcp-abap-adt/coreABAP ADT MCP server published as an npm scope and plugin
  11. chandrashekhar-mahajan/abap-mcp-serverABAP MCP server with ADT tools and five authentication strategies
  12. dassian-adtFork of mcp-abap-abap-adt-api with 25 tools and session recovery
  13. erpl-adtC++ binary speaking the ADT REST API as a CLI and MCP server
  14. bw-modeling-mcpMCP server for BW/4HANA modelling objects
  15. EPOD ADT MCP ServerMCP server for ABAP on the ADT session, in Eclipse or standalone
  16. ZARC1_EXTENSION_FORMSarc-1 extension for SAPscript and Smart Forms objects
  17. fgalastri/MCP_ABAPValidates an ABAP class before it is deployed by converting the global class to a local one and syntax-checking that — plus CDS DDL and table metadata retrieval.
  18. fr0ster/mcp-abap-adtABAP MCP server with full CRUD on on-premise and ABAP Cloud
  19. abapsmithABAP MCP server over ADT with a local journal that can undo writes
  20. sap-ai-dev-toolkitMCP proxy pinning an agent's ABAP tools to one SAP system
  21. aibap.mcpGo MCP server that writes, activates and runs ABAP classes via ADT
  22. BellaEclipse plugin bringing Claude into ABAP Development Tools
  23. Maherd18/sap-mcp-serverPolicy-checked MCP server for ABAP development over ADT
  24. sapdumpmcpMCP server reading ABAP short dumps over ADT
  25. ABAP remote filesystemVS Code extension editing ABAP systems over ADT as a filesystem
  26. mcp-abap-abap-adt-apiWrite-oriented MCP server over abap-adt-api
  27. mcp-abap-adtRead-oriented MCP server fetching ABAP programs, classes, tables
  28. SAP MCP Connection ManagerWindows desktop manager handing SAP connections to assistants over MCP
  29. ABAPilotMCP connector for ECC and on-premise S/4HANA with a licensed SAP side
  30. vibing-steampunkADT-to-MCP bridge in Go reaching past what ADT exposes
  31. ADT MCP ServerLocal HTTP MCP server started from ABAP Development Tools for VS Code
  32. ABAP Development Tools for VS CodeSAP's ABAP tools in VS Code for editing, debugging, ATC and transports
  33. SAP Joule for Developers, ABAP AI capabilitiesSAP's AI for ABAP development in ADT for Eclipse and VS Code
  34. ABAPDocMCPMCP server turning a transport request into a WRICEF specification
  35. claude-sap-kitClaude Code plugin adding Custom_* tools to arc-1
  36. Skybuffer AI MCP serverABAP add-on serving business data and transactions as MCP tools
  37. workskong/mcp-abap-adtLight MCP adapter for ABAP over ADT, documented in Korean
  38. KlystronMCP server in ABAP inside the application server, with agent skills
  39. pyrfc-mcpMCP server calling RFC-enabled function modules behind an allowlist
  40. ABAP-AI-CODE-TOOLSabapGit package adding create, modify and delete tools to ABAP AI Code
  41. ABAP AI CodeAgentic coding assistant in ABAP that runs as an SAP GUI report
  42. VERTEXADT extension for Eclipse and VS Code showing three SAP GUI tools

Details

Sources3
Added by handdiscovered herechecked
github.com/vaibhavgoel-github-1986/abap-adt-clitree, pushes, contributors, CIchecked
README.mdquoted, and what the entry declareschecked

Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.

Compatibility and provenance
discovered viaAdded by hand
first seen in the atlas
ecosystem and domainabap, developer-experience
Contributors1

From the repository’s contributor list, .

History in the atlas1
  1. First seen.
For your agent
Ref
artifact:vaibhavgoel-github-1986-abap-adt-cli
JSON
/artifacts/vaibhavgoel-github-1986-abap-adt-cli.json
MCP
Connecting an agent