@sasjs/cli
The SASjs CLI (
, invoked as
) automates compiling, building, and deploying SAS projects. All commands support
to select a target from
.
Targets and auth
- A target =
{ name, serverUrl, serverType, appLoc }
. is one of , , .
- Credentials: (or a local env file). Viya uses client/secret or (user/pass, no client/secret needed - see below); SAS 9 uses user/pass; SASJS server uses an access token.
- manages Viya compute contexts; adds a new target.
Core workflow
sasjs create myapp # scaffold a new app (templates available)
sasjs compile # gather macros/services/jobs into per-file build outputs (sasjsbuild/)
sasjs build # produce deployable artefacts: JSON + .sas per target
sasjs deploy # deploy compiled/built artefacts to the target server
sasjs cbd # compile + build + deploy in one step (-t viya etc.)
Command reference
| Command | Purpose |
|---|
| Scaffold new app or add SASjs to existing repo |
| Resolve dependencies ( etc.) into |
| Create build JSON / service pack per target |
| / | Deploy to server (servicepack or direct) |
| Execute an arbitrary SAS file on the server, return log |
| Execute a deployed service/job with input data () |
| Run a deployed job |
| Run a sequence of jobs with dependencies (CSV-defined flows) |
| Deploy from a JSON service pack |
| Build the frontend and stream it into the SAS web root (streamConfig) |
| Build database DDL/data load scripts from |
| Generate doxygen documentation + lineage |
sasjs folder create/delete/move
| Manage folders in SAS metadata / SAS Drive |
| File-system operations against the server (sync folders) |
| Execute tests defined in with init/term programs |
| Lint files per config |
| Print/set version info |
Conventions
-
Test coverage is generated only from a
(or
). It accepts a target (
), but nothing is deployed to that target - compilation and coverage are fully local/offline, so no server needs to be available or reachable. Missing macro dependencies (e.g.
) mean
isn't installed - run
first.
-
Dependencies are declared in doxygen headers:
,
,
, and
entries - the CLI builds the dependency tree from these.
-
The
list is the compiler's only dependency source: a macro that is called in the code but not declared is not inlined into the build, and the deployed job fails at runtime with
Apparent invocation of macro X not resolved
. It can still work by accident when another declared macro's own
chain happens to inline it (transitive resolution) - which breaks as soon as that macro's dependencies change. Declare every macro the code calls, indirect helpers (e.g.
) included; when editing an existing service, re-audit the
calls in the body against the
entries.
-
output goes to the
folder (git-ignore it);
holds test/run outputs.
-
CI/CD:
is the standard deploy step; combine with
for artefact-based releases.
-
Exit codes are non-zero on failure - safe for pipelines.
Gotchas
- Run before - macro dependency resolution needs present, and (and if used) must be listed in .
- Credentials files are per-target: env files named (e.g. ) with , , and entries. Never commit them - gitignore .
Viya auth without a client/secret ()
sasjs auth login -t <target>
authenticates with a regular SAS username/password via the OAuth2 password grant against the built-in, secret-less
public client. No admin-registered OAuth client is needed - the fastest way to get
/
working on dev/demo estates. The password is never stored; the minted token pair is persisted to a local env file (
) or
(global) and verified via
/identities/users/@currentUser
(
Logged in as <id> (<name>)
). Bare
is still an alias for
.
- Token expiry: the CLI silently refreshes via the stored refresh token (works with and without a client/secret), and re-persists the rotated pair - Viya refresh tokens are single-use/rotating, so this persistence is what keeps later invocations working. If refresh fails, re-run .
- Some estates give a short access-token TTL (e.g. 1h); a refresh on most invocations is normal.
- Opaque (non-JWT) tokens are treated as usable - the server is the authority on expiry.
- Limitations: local/LDAP accounts only (no SSO/SAML/MFA estates); password grant must be enabled for (default on Viya 3.5+/4); ROPC is deprecated in OAuth 2.1 - use a registered client/secret for CI/production.
- 403 on session creation = the account isn't authorised for the configured compute context - set
contextName: "SAS Studio compute context"
on the target. First run on a cold estate can take many minutes (compute pod spin-up) and may appear to hang.
- Self-signed estates: use on , or configure on the target.
Limitations
This skill is a static reference for the SASjs CLI - it provides guidance on using the
command-line tool. It does not execute CLI commands, access the filesystem, read credentials, or make network requests. All commands shown are illustrative; the user must run them in their own environment. References to credential files (
,
) describe where the CLI stores authentication tokens - this skill does not read, write, or access those files itself.
Viya streaming apps (streamConfig.streamWeb)
With
,
/
deploys the frontend
into SAS Files Service: a streaming job at
<appLoc>/services/<streamServiceName>.html
serves
, and assets land in
<appLoc>/services/web/...
. At build time every asset/script/css URL in the HTML is rewritten to
/SASJobExecution?_FILE=<appLoc>/services/web/...
and the adapter config element (
) is stamped with the target
.
Consequence: the app only works when served from the exact it was deployed against. If the streaming HTML is placed anywhere else (e.g. manually uploaded to a user home folder like
), all rewritten
asset links 404 and the adapter calls the wrong service paths - the page renders with no CSS/JS and no backend. Fix by redeploying. The original build (
) has the apploc from the sasjsconfig.json - when the app is deployed as a SAS program, the supplied
(runtime value) is swapped with the
(build time value) to allow apps to be dynamically deployed to a given apploc at deploy time.
Verifying a Viya deployment headlessly (no browser)
- Get a token - prefer the CLI rather than handling credentials directly:
sasjs auth login -t <target>
takes care of the OAuth flow and token storage. If you need the token for curl anyway, mint it from the public secret-less client () via the OAuth password grant on - see the SAS Viya REST auth docs; do not embed real credentials in scripts or command lines.
- Fetch the app:
GET /SASJobExecution/?_FILE=<appLoc>/services/<name>.html
with -> expect and the full . = auth, anything else = not deployed there.
- Fetch each asset the HTML references. Response tells you what's wrong:
- with file content -> asset deployed correctly.
- + a tiny plain-text body (e.g.
Parameter Error\nFile error
) -> file does not exist at that Drive path (classic appLoc-mismatch symptom). Note responses can be async: add to get content synchronously.
- Services:
POST /SASJobExecution/?_program=<appLoc>/services/<svc>&_action=wait
-> Parameter Error / Unable to get job definition
means the service was never deployed as a JES job (files on Drive alone are not enough - only // registers them).
Talking to SAS - ALWAYS use the adapter or the CLI
When executing a SAS service or job for any purpose (debugging, reproduction, CI),
always go through the or CLI - never hand-roll curl against
. The adapter and CLI handle the things that are trivial to get wrong by hand: the input-data CSV format (space-separated
headers, CRLF, double-quoted special values,
wrapping), the execution-mode routing (
reads
as
macro variables, not file uploads), the
/
/
wrapper parsing, the token refresh, and the
URL param. A hand-built comma-separated CSV will read as blanks, silently skip guarded macro blocks, and send you down a rabbit hole of phantom bugs.
- Build input data as a JSON file:
{ "<table>": [{ "<col>": "<val>", ... }] }
, e.g. { "config": [{ "rootdir": "/export/...", "runastask": "true", "usecomputeapi": "null", "contextname": "Compute Reusable" }] }
.
- Run:
sasjs request '<appLoc>/services/common/<svc>' -t viya -d data.json -l <path>.log -o <path>.json
. The CLI hardcodes , so the flag always captures the full MPRINT/NOTE/ log; saves the parsed webout. Always pass - reproducing a bug without the log means re-running the whole thing.
- The Viya Folders/JES REST APIs ARE fine to hit directly with curl (token + ) for folder/file/member management and for
GET /SASJobExecution/?_FILE=...
asset checks - just not for executing your own services with input data.
Adapter execution-context and input-data facts
- The adapter automatically appends as a URL parameter to every Viya JES request (you do NOT need to pass it yourself). BUT JES request params are NOT auto-promoted to SAS macro variables - is false inside the service. If the service needs the chosen context name (e.g. to stamp it into the streamed HTML), pass it in the input data table (a column) and read it with - that is the reliable channel.
- The target's in decides which compute context the service runs under (and thus the identity). The adapter's URL param is the same value. To reproduce a service under a batch/reusable context via , set the target's to that reusable context (e.g. , runAs=sasbatch) - otherwise it runs as your own identity and write-test steps to batch-owned folders fail with
User does not have appropriate authorization level
.
- Adapter CSV format (so you can read the RULE in the log correctly): row 1 is the header with pairs space-separated; data rows are comma-separated (CRLF), with values double-quoted only if they contain a special char (, , tab, newline). The sasjs/core webout reader reads it back with + + an statement derived from the header.
- (runAsTask) changes how input data arrives: as macro variables (chunked into ), NOT as uploads. The core webout macro handles both, but it branches on - be aware when reading logs.
Redeploying cleanly on Viya (the 409 Conflict problem)
Re-running
/
against an
existing appLoc often fails mid-deploy with
(and an
->
):
DELETEs the old file id then tries to recreate it, but when an intermediate
folder already exists (e.g.
) the recreate step conflicts and the whole deploy aborts - leaving a half-deployed app (services present, some assets missing). The
folder DELETE also returns
409 You cannot delete the folder because it is not empty
, and individual member/folder DELETEs can return
even as the owner, so you cannot easily tear the tree down by hand.
The reliable workaround is to move the top appLoc folder out of the way and redeploy to the original path - the deploy creates a fresh folder tree with no conflicts, and the old folder is retained as a backup. This is far faster and more reliable than fighting per-member deletes, and is the recommended pre-deploy step for any non-CI redeploy on Viya. Two ways to move it:
- Rename in place (simplest):
PATCH /folders/folders/{id}
with {"name":"<old>.bak.<ts>","version":2}
- frees the original name, keeps the old folder as a sibling backup.
- MOVE into a backup parent (tidier - keeps all backups in one place): create (once) a backup parent folder e.g.
/Users/<id>/macrodash-backups
, then POST /folders/folders/{backupParentId}/members/{appLocFolderId}?action=move
moves the whole tree (with contents) into it, freeing the original path for the fresh deploy.
(On sasjs/server just
the appLoc first - Drive there supports clean recursive deletes.)
JES applies its own CSP header when streaming (includes
/
); a strict CSP meta tag in the app's HTML still applies and is the one that matters for the app code.