Use Qualia from an agent
An agent uses Qualia with an API key, through the CLI or the SDK. A key acts as the person who made it, inside one organization. It can do what that person’s role allows, and its scopes can only narrow that.
This page walks the path from an upload to an export. Then it covers finding your way around, admin work, and what a key cannot do. Each step shows the CLI first, then the same work in Python. The command reference and the SDK pages list every option.
Pass --json for output a script can read. A command that fails exits 1 with
the reason on stderr. A command used wrongly exits 2.
Make a key in the dashboard: open Settings, then Account › API keys, click New key, then Create key. Copy the key: it is shown once. Keys expire after 90 days by default. No key can make another key.
The key acts in the workspace it was made in. QUALIA_WORKSPACE picks another
workspace you can reach. The reference pages call it an organization key. Every
step on this page works with it.
A key restricted to some workspaces (a workspace key) reaches much less for now. It lists only its own workspaces, and it is refused on datasets, uploads, tasks, checks, activity and running work. It cannot change or delete a workspace.
A key carries every scope except three admin scopes, so it can upload and curate
as soon as it exists. The three are members:read, members:write and
integrations:write. None is on by default. Open More options and tick
one under Admin (opt-in) when you make the key. A key with * does not get them. See
Admin scopes.
Give the key to the CLI once:
qualia auth login --base-url https://dev-api.qualiastudios.devqualia auth statusqualia auth status shows what is stored. It does not call the API, so the
first command that does is the one that proves the key works. Every command
acts in the key’s own account, so none needs an account id
(Your account).
An agent usually gets its key from the environment instead. Set QUALIA_API_KEY
and QUALIA_BASE_URL together, because the CLI ignores its config file once the
key is set. QUALIA_WORKSPACE picks the
workspace to act in, if it is not the key’s own.
Scripts and CI has the details.
The SDK reads the same variables:
from qualia import Qualia
client = Qualia()The path
Section titled “The path”1. Upload
Section titled “1. Upload”qualia dataset upload ./session-0412 session-0412 --standalone --json > receipt.jsonDATASET=$(jq -r .dataset_id receipt.json)qualia dataset ingest-report "$DATASET"The later steps reuse $DATASET. The upload’s exit code says what it came to: 0
published, possibly with flagged episodes; 1 it failed or was refused; 3 the
bytes are kept but no episode could be derived.
qualia dataset upload lists the
formats.
Exit 1 does not mean nothing is left. A transfer that fails partway stays in the
upload feed as failed, and the bytes it stored stay too. Read the feed before
you try again:
qualia dataset uploads works from any
machine. cancel-upload stops an
upload in flight.
dismiss-upload takes a failed
one off the feed and leaves its bytes.
--standalone keeps the upload on its own. Without it, a machine with a saved
destination appends the upload to
that merged dataset. The upload is then part of that dataset, and the server
refuses the steps below on it.
receipt = client.data.upload("./session-0412", name="session-0412", standalone=True)dataset = receipt["dataset_id"]report = client.datasets.ingest_report(dataset)2. Set up the robot and gripper
Section titled “2. Set up the robot and gripper”ROBOT=$(qualia robot create lab-arm --urdf lab_arm.zip --json | jq -r .name)qualia robot model "$ROBOT"qualia robot configure "$ROBOT" --solver dlsGRIPPER=$(qualia gripper create parallel_gripper.urdf --json | jq -r .name)qualia gripper tcp "$GRIPPER" tcp.jsonqualia robot carry "$ROBOT" "$GRIPPER"qualia dataset update "$DATASET" --target-robot "$ROBOT"qualia robot create with --urdf
imports the file and waits for it.
qualia robot model shows the arms it
guessed, and qualia robot configure
corrects them. qualia gripper tcp says
where the tool centre point is.
qualia robot carry gives the robot its own
copy of the gripper, so change the copy from then on.
qualia dataset update picks the robot
the dataset plays on.
robot = client.robots.create_from_urdf("lab_arm.zip")model = client.robots.get_model(robot.name)client.robots.update_model(robot.name, solver="dls", etag=model.etag)
gripper = client.robots.create_from_urdf("parallel_gripper.urdf", kind="gripper")client.robots.set_gripper_tcp(gripper.name, file="tcp.json")client.robots.set_gripper(robot.name, gripper.name)client.datasets.update(dataset, target_robot=robot.name)See robots.
3. Link a task
Section titled “3. Link a task”A task says what the robot should do and how success shows. Publish it, then link the upload to it.
TASK=$(qualia task create --name "Cube in box" -d "Pick the cube off the table and put it in the box" --json | jq -r .name)qualia task edit "$TASK" --success "The cube rests inside the box"qualia task publish "$TASK" -m "First revision"qualia dataset link-task "$DATASET" "$TASK"qualia task has the rest.
task = client.tasks.create("Cube in box")client.tasks.update( task.name, description="Pick the cube off the table and put it in the box", success="The cube rests inside the box",)client.tasks.publish(task.name, note="First revision")client.datasets.update(dataset, linked_task=task.name)See tasks.
4. Run checks
Section titled “4. Run checks”A check never keeps, rejects or deletes an episode. It raises issues and notes.
The catalog’s checks use a model and cost credits, so see their bound first:
--validate-only starts nothing.
qualia dataset checks list "$DATASET"qualia dataset checks run "$DATASET" --all --validate-onlyqualia dataset checks run "$DATASET" --all --max-cost 200 --waitqualia dataset checks findings "$DATASET" --verdict FAILqualia dataset issues summary "$DATASET"See qualia dataset checks and
qualia check, which also reads check runs,
reference ranges and the reach check.
started = client.checks.run(dataset, all=True, max_cost=200)run = client.checks.wait(started.check_run)findings = client.checks.list_findings(dataset, verdict="FAIL")See checks.
5. Curate
Section titled “5. Curate”Read what is there, then rule. Pick episodes by id or with the filter the dataset page’s filter bar writes.
qualia dataset summary "$DATASET" --facet state --facet open_issue_codesEPISODE=$(qualia dataset episodes "$DATASET" --filter 'state = UNREVIEWED' -n 1 --json | jq -r '.episodes[0].name')qualia dataset keep "$DATASET" --filter 'state = UNREVIEWED AND open_issue_count = 0' --reason "no issues"qualia dataset reject "$DATASET" --filter 'metrics.duration_seconds < 2' --reason "too short"qualia dataset reset "$DATASET" "$EPISODE"keep and
reject never delete anything, and
reset withdraws them. A take a person
has open in the studio is left as it was: the command lists it and still exits 0. Run it again once they are done.
Issues are settled apart from rulings:
qualia dataset issues list "$DATASET" --filter 'state = OPEN AND blocks = true'qualia dataset issues dismiss "$DATASET" --filter 'code = "idle_tail"' --reason "fine for this task"qualia dataset issues add "$DATASET" "Gripper slips at the second grasp" -e "$EPISODE" --at 12.5Edits change the take, not a ruling. qualia dataset trim --clear restores a
trim, and qualia dataset unsplit removes what a split made. --dry-run plans a
split and writes nothing.
qualia dataset edit "$EPISODE" -d "$DATASET" --outcome SUCCESSqualia dataset trim "$EPISODE" -d "$DATASET" --start 1.5 --end 12qualia dataset split "$EPISODE" -d "$DATASET" --keep 0.5-6 --keep 8-14 --dry-runSee qualia dataset edit,
trim and
split.
page = client.episodes.list(dataset, filter="state = UNREVIEWED", limit=20)episode = page.items[0].nameclient.episodes.keep( dataset, filter="state = UNREVIEWED AND open_issue_count = 0", reason="no issues")client.episodes.reject(dataset, [episode], reason="operator in frame")client.episodes.reset(dataset, [episode])client.issues.dismiss(dataset, filter='code = "idle_tail"', reason="fine for this task")client.episodes.update(episode, dataset=dataset, outcome="SUCCESS")6. Version
Section titled “6. Version”A version freezes what the dataset yields. Training and downloads read versions, never the live dataset. Read the diff first, then tag the head you reviewed: the tag is refused if the head moved since.
DIGEST=$(qualia dataset version diff "$DATASET" --json | jq -r .targetDigest)qualia dataset version create "$DATASET" 1.0 --note "first cut" --digest "$DIGEST"qualia dataset version list "$DATASET"Tags never move. qualia dataset pin makes
the same tag and also freezes a dataset definition. See
qualia dataset version.
diff = client.versions.compare(dataset)tag = client.versions.create(dataset, "1.0", note="first cut", digest=diff.target_digest)See versions.
7. Export
Section titled “7. Export”qualia dataset download "$DATASET:1.0" ./session-0412-1.0The CLI checks every file against its size and sha256, shows progress on
stderr, and creates the folder only once the whole tree is there. A run that
stops picks up where it left off. The first download of a version in a format
starts its rendering, which needs a key that can write to the dataset
(data:write); once it is rendered, downloads need read access only
(data:read).
--format takes lerobot_v3 (the default), lerobot_v2_1, mcap_ros2 or
original (every file uploaded to the dataset, byte for byte; no version, and
an organization key for now). See
qualia dataset download and
Download a version.
result = client.data.download(f"{dataset}:1.0", "./session-0412-1.0")Find your way around
Section titled “Find your way around”| To see | CLI | SDK |
|---|---|---|
| Workspaces | qualia project list | client.projects.list() |
| One workspace | qualia project show | client.projects.get(project_id) |
| What people and keys did | qualia activity list | client.activity.list() |
| Running work | qualia status list | client.statuses.list() |
| Stop running work | qualia status cancel | client.statuses.cancel(name) |
qualia activity list --org --filter 'verb = KEEP AND actor = API_KEY' --jsonqualia status list --running --jsonAdmin work needs an opt-in scope ticked when the key was made. The scope opens
the door. The role of the key’s creator still decides: a Contributor’s key with
members:write cannot change a role, and no key grants a role above its
creator’s own. A guest’s key can do none of this. Activity records each write
with the key’s name. credits:write is an ordinary scope that every new key
holds.
- People and roles (
members:read,members:write):qualia memberlist,show,role,remove;client.memberslist,get,set_role,remove. - Invitations (
members:read,members:write):qualia invitationlist,send,withdraw;client.invitationslist,send,withdraw. - Integration keys (
integrations:write):qualia integrationlist,add,remove,default;client.integrationslist,add,remove,set_default. - Vouchers (
credits:write):qualia credit redeem;client.credits.redeem(code).
An integration key’s value goes in once and is never shown again, so pipe it in and keep it out of your shell history:
printf %s "$HF_TOKEN" | qualia integration add huggingface --name ci --secret-stdinWhat a key cannot do
Section titled “What a key cannot do”These stay in the dashboard:
- Log in or sign up. A person makes the key.
- Reset a password. That is the person’s.
- Change appearance. Theme is a browser setting.
- See presence. Who is on a page is shown to signed-in people.
- Hold an edit lease. A person editing a take blocks a key’s write to it.
- Buy credits. Stripe checkout needs a person. A key can only redeem a voucher.
- Create or delete an organization. A person does that.
Try the whole path
Section titled “Try the whole path”sdk/cli/examples/agent_path.sh, in the CLI’s source, runs this path with
--json against the stack named by QUALIA_BASE_URL and QUALIA_API_KEY. It
makes its own data, prints a line per step, and exits non-zero if a step fails.
Where a stack cannot finish a step, it says so.
On exit it deletes what it made and cancels an upload it did not finish. An upload’s repository and stored bytes stay, because no command deletes them. The script prints a line for each one.