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 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.3.0 shFirst run
On macOS the installer starts onboarding in an interactive terminal: it creates a profile and stores the token once in the Keychain. 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 |
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