A Foundry CLI
built for agents.
One Rust binary for Palantir Foundry, with structured JSON output and plan-first writes.
curl -fsSL https://github.com/zaycruz/orthanc/releases/latest/download/install.sh | shcurl -fsSL https://github.com/zaycruz/orthanc/releases/download/nightly/install.sh | \
ORTHANC_CHANNEL=nightly ORTHANC_INSTALL_DIR="$HOME/.local/orthanc-nightly/bin" shSHA-256 verified. Installs to ~/.local/bin. No GitHub account required. More install options
Plan-first writes: create a folder, read it back, delete it. Real commands and output, recorded live against a Foundry sandbox with Orthanc 0.3.0. The Foundry window beside it was recorded during the same run, on the same clock; long waits play at 20× on both sides. Everything created is deleted afterwards.
Transcript
# agent: make a folder for Q3 docs. Plan first.
$ orthanc folder create q3-docs -p $P
{
"mode": "plan",
"request": {
"body": {
"displayName": "q3-docs",
"parentFolderRid": "ri.compass.main.folder.f23e9fb1-b130-401d-9612-c6b8a1edb159"
},
"path": "/v2/filesystem/folders",
"verb": "POST"
}
}
# agent: plan looks right; apply it
$ F=$(orthanc folder create q3-docs -p $P --apply | jq -r .rid)
$ orthanc folder get $F | jq "{display_name, path}"
{
"display_name": "q3-docs",
"path": "/Sandbox-c0c396/orthanc-e2e/q3-docs"
}
# agent: done with it. Plan the delete.
$ orthanc resource delete $F
{
"mode": "plan",
"request": {
"body": null,
"path": "/v2/filesystem/resources/ri.compass.main.folder.bfcd49bd-7d5c-4113-8300-78a84234f830",
"verb": "DELETE"
}
}
# agent: apply the delete (moves it to trash)
$ orthanc resource delete $F --apply >/dev/null
$ orthanc resource get $F | jq .trash_status
"DIRECTLY_TRASHED"
# agent: the project is back as it was# agent: add distance_km to the flights transform
$ git status -s
M transforms-python/src/myproject/datasets/flights_enriched.py
$ cat */src/*/datasets/flights_enriched.py
import polars as pl
from transforms.api import Input, Output, transform
ROOT = "/Sandbox-c0c396/orthanc-e2e/flights-demo"
@transform.using(
out=Output(f"{ROOT}/flights_enriched"),
flights=Input(f"{ROOT}/flights"),
)
def compute(flights, out):
km = (pl.col("distance_mi") * 1.609).round().cast(pl.Int64)
out.write_table(flights.polars(lazy=True).select(
"flight_id", "carrier", "origin", "dest", "distance_mi",
km.alias("distance_km"), "dep_delay_min",
))
# agent: commit and push to the Foundry repo
$ git commit -am "flights: add distance_km"
[master 159cf9c] flights: add distance_km
1 file changed, 5 insertions(+), 1 deletion(-)
$ orthanc repository push $R $M $M --allow-default-branch \
> --apply | jq "{status, remote_commit}"
{
"status": "pushed",
"remote_commit": "159cf9c30372c41dcc24d86e129c626c0c0ef95a"
}
# agent: wait for CI (ci/foundry-publish registers the job)
$ C=$(git rev-parse HEAD)
$ orthanc repository checks wait $R $C | cut -d" " -f1-3
state: passed
ci/foundry-publish OK refs/heads/master
# agent: plan the build of the output dataset
$ orthanc orchestration builds create "$T" \
> | jq ".mode, .request.body.target.targetRids[]"
"plan"
"ri.foundry.main.dataset.6042f8e6-7713-4d40-96ed-124069f4365a"
# agent: plan is right; start the build and watch it
$ B=$(orthanc orchestration builds create "$T" --apply \
> | jq -r .build.rid); echo $B
ri.foundry.main.build.41b4e619-a3cb-4fee-862d-c59a7fda68a1
$ while sleep 3; do s=$(orthanc orchestration builds get $B \
> | jq -r .build.status); echo "$(date +%T) $s"
> [ "$s" = RUNNING ] || break; done
08:17:56 RUNNING
08:17:59 RUNNING
08:18:02 SUCCEEDED
# agent: check the new column in the output
$ orthanc dataset preview $OUT | jq -r .content \
> | tr -d \" | cut -d, -f1,2,5,6 | column -ts,
flight_id carrier distance_mi distance_km
UA101 UA 2586 4161
UA204 UA 888 1429
DL310 DL 1946 3131
DL422 DL 2421 3895
AA515 AA 1121 1804
AA630 AA 2611 4201
# agent: done; distance_km is live in flights_enriched# agent: model the pipeline output as a Flight object type
$ jq ".objectTypes[].create.objectType.apiName" flight.json
"Flight"
$ jq "..|.datasetRid? // empty" flight.json
"ri.foundry.main.dataset.6042f8e6-7713-4d40-96ed-124069f4365a"
$ FL=dsutbk8l.flight; CA=dsutbk8l.carrier
# agent: dry run first, then apply
$ orthanc ontology object-type-create $ONT \
> --request-file flight.json | jq "{mode, validation}"
{
"mode": "dry-run",
"validation": {
"success": {},
"type": "success"
}
}
$ orthanc ontology object-type-create $ONT \
> --request-file flight.json --apply \
> | jq -c "{created: .result.created_object_types | keys}"
{"created":["dsutbk8l.flight"]}
$ orthanc ontology object-type-create $ONT \
> --request-file carrier.json --apply | jq .change_status
"changed"
# agent: read the Flight definition back
$ orthanc ontology definition $ONT --object-type $FL \
> | jq -c ".definition.objectType.propertyTypes[]
> | [.apiName, .displayMetadata.displayName]"
["depDelayMin","Delay (min)"]
["flightId","Flight ID"]
["dest","Dest"]
["distanceMi","Distance (mi)"]
["carrier","Carrier"]
["origin","Origin"]
# agent: expose the new distance_km column
$ orthanc ontology object-type-add-property $ONT --apply \
> --object-type $FL --api-name distanceKm --type long \
> --column distance_km --display-name "Distance (km)" \
> | jq "{mode, change_status}"
{
"mode": "applied",
"change_status": "changed"
}
$ orthanc ontology definition $ONT --object-type $FL \
> | jq -c ".definition.objectType.propertyTypes[]
> | [.apiName, .displayMetadata.displayName]" | grep -i km
["distanceKm","Distance (km)"]
# agent: link Carrier -> Flights, dry run then apply
$ LK=dsutbk8l.carrier-flights
$ orthanc ontology link-type-create $ONT \
> --request-file link.json | jq .validation.type
"success"
$ orthanc ontology link-type-create $ONT \
> --request-file link.json --apply \
> | jq -c "{created: .result.created_link_types | keys}"
{"created":["dsutbk8l.carrier-flights"]}
$ orthanc ontology definition $ONT --link-type $LK \
> | jq -c ".definition.linkType.definition.oneToMany
> | [.cardinalityHint, (..|.apiName? // empty)]"
["ONE_TO_MANY","operatedBy","flights"]
# agent: clean up. Dry-run the link delete first.
$ orthanc ontology link-type-delete $ONT \
> --request-file del-link.json | jq .validation.type
"success"
$ orthanc ontology link-type-delete $ONT \
> --request-file del-link.json --apply | jq .change_status
"changed"
$ orthanc ontology object-type-delete $ONT \
> --request-file del-types.json --apply | jq .change_status
"changed"
# agent: confirm all three are gone
$ for t in $FL $CA; do orthanc ontology definition $ONT \
> --object-type $t 2>&1 | grep -o "has no.*"; done
has no object-type 'dsutbk8l.flight'.
has no object-type 'dsutbk8l.carrier'.
$ orthanc ontology definition $ONT --link-type $LK \
> 2>&1 | grep -o "has no.*"
has no link-type 'dsutbk8l.carrier-flights'.
# agent: done; the Sandbox Ontology is empty again# agent: setBedStatus v2 also fixes the case. The source:
$ orthanc container exec $D "cat $F" --apply | jq -r .stdout
import { Client, Osdk } from "@osdk/client";
import { createEditBatch, Edits } from "@osdk/functions";
import { BedStatus } from "@ontology/sdk";
type Edit = Edits.Object<BedStatus>;
export default function setBedStatus(
client: Client,
bed: Osdk.Instance<BedStatus>,
status: string,
): Edit[] {
const batch = createEditBatch<Edit>(client);
const t = status.trim().toLowerCase();
const next = t.charAt(0).toUpperCase() + t.slice(1);
if (bed.status !== next) {
batch.update(bed, { status: next });
}
return batch.getEdits();
}
# agent: preview W10-B16 (Vacant), status " cLEANING "
$ orthanc functions preview $D --file-path $F \
> --method-name setBedStatus --parameters "$P" | jq -c \
> ".outcome, (..|.modify_object?|values|.locator.primary_key,
> .property_values)"
"success"
{"bed_id":"W10-B16"}
{"status":"Cleaning"}
# agent: commit and push it to master
$ orthanc container exec $D --apply "git add $F &&
> git commit -qm \"setBedStatus: fix the case\" &&
> git push -q origin HEAD:master && git log --oneline -1" \
> | jq -r .stdout
c484a67 setBedStatus: fix the case
$ C=$(orthanc container exec $D "git rev-parse HEAD" \
> --apply | jq -r .stdout); echo $C
c484a67e619252e20bada0a5ca374f99466b2793
# agent: 0.0.2 is the last release. Tag 0.0.3, plan first.
$ orthanc repository tag create $R refs/tags/0.0.3 \
> --commit-hash $C | jq -c "{status, tag: .intended_body
> | .requests[].variables.tagName}"
{"status":"dry-run","tag":"refs/tags/0.0.3"}
$ orthanc repository tag create $R refs/tags/0.0.3 \
> --commit-hash $C --apply | jq -c "{status, tag: .tag.name}"
{"status":"created","tag":"refs/tags/0.0.3"}
# agent: wait for ci/functions-publish on the tag
$ orthanc repository checks wait $R refs/tags/0.0.3 \
> --timeout 600 | tee w.txt | sed -E "s/ 20[0-9-]+T.*//
> s/^ +//; s/(tion\.[0-9a-f]{8})[^ ]*/\1.../"
state: passed
ci/functions-publish OK refs/tags/0.0.3
published ri.function-registry.main.function.ccb26f67... 0.0.3
# agent: which function version did the build publish?
$ B=$(grep -o "build=[^ ]*" w.txt | cut -d= -f2)
$ for i in $(seq 60); do orthanc repository build-status $B \
> | sed -E "s/^ +//; s/(tion\.[0-9a-f]{8})[^ ]*/\1.../" \
> | grep published && break; sleep 5; done
published ri.function-registry.main.function.ccb26f67... 0.0.3
$ FN=$(orthanc repository build-status $B \
> | grep -o "ri.function-registry[^ ]*")
# agent: list the function's published versions
$ orthanc functions versions $FN | jq -c "{display_name,
> count}, (.versions[] | {version, time: .registered_time})"
{"display_name":"setBedStatus","count":2}
{"version":"0.0.3","time":"2026-09-25T12:23:00.637653891Z"}
{"version":"0.0.2","time":"2026-09-25T02:46:25.365453142Z"}
# agent: done; setBedStatus 0.0.3 is publishedPlan first
Every command that changes Foundry prints the exact request as a plan and changes nothing until you add --apply.
One envelope
Add --agent and any command, including a parse error, returns one foundry-agent-v1 object: data, errors, warnings, pagination, always in the same place.
Typed errors
Every error has a type and a nonzero exit code. API errors carry Foundry's error name and say whether a retry can help.
Credentials out of band
Tokens are stored once per profile, in the macOS Keychain or the Linux Secret Service (a 0600 file on a headless host), and read from stdin or the environment. They never enter argv, logs, or output.
Install
Two channels, one installer.
Stable
Latest tagged release.
curl -fsSL https://github.com/zaycruz/orthanc/releases/latest/download/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
orthanc --versionNightly
Rebuilt after every merge to main. Published as a prerelease. Installs beside stable.
curl -fsSL https://github.com/zaycruz/orthanc/releases/download/nightly/install.sh | \
ORTHANC_CHANNEL=nightly ORTHANC_INSTALL_DIR="$HOME/.local/orthanc-nightly/bin" sh
"$HOME/.local/orthanc-nightly/bin/orthanc" --versionPinned
Any stable tag via ORTHANC_VERSION.
curl -fsSL https://github.com/zaycruz/orthanc/releases/latest/download/install.sh | \
ORTHANC_VERSION=v0.7.3 shFirst run
In an interactive terminal the installer starts onboarding: it creates a profile and stores the token once, in the Keychain on macOS and in the Secret Service on Linux, or in a 0600 file on a headless Linux host. Set ORTHANC_SKIP_ONBOARDING=1 to defer.
orthanc onboarding
orthanc configure set-active PROFILEManual download and verification
| Target | Archive | Checksum |
|---|---|---|
| macOS, Apple Silicon | orthanc-aarch64-apple-darwin.tar.gz | .sha256 |
| macOS, Intel | orthanc-x86_64-apple-darwin.tar.gz | .sha256 |
| Linux x86_64, glibc ≥ 2.35 | orthanc-x86_64-unknown-linux-gnu.tar.gz | .sha256 |
shasum -a 256 -c orthanc-aarch64-apple-darwin.tar.gz.sha256
tar -xzf orthanc-aarch64-apple-darwin.tar.gz
install -m 755 orthanc ~/.local/bin/orthanc