Skip to content

Commands

These flags go before the command and work with any command that calls the API. Each one overrides its environment variable, which overrides the config file that qualia auth login writes.

OptionEnv varDescription
--tokenQUALIA_API_KEYAPI key
--base-urlQUALIA_BASE_URLAPI origin, e.g. https://dev-api.qualiastudios.dev (default https://api.qualiastudios.dev)
--accountQUALIA_ACCOUNTOptional. Account UUID to act on; default is the API key’s own account (more)
--workspaceQUALIA_WORKSPACEWorkspace UUID to act in instead of the API key’s own workspace; never stored in the config file; rarely needed
Terminal window
qualia --token <API_KEY> credit
qualia task list # the API key's own account; no --account needed

qualia auth status shows the stored config and the environment variables, not these flags.


Show CLI and SDK versions. qualia --version prints the same.

Terminal window
qualia version
qualia --version

Save your API key, and optionally the API origin and an account, to the config file. Prompts for the key when --api-key is left out, and hides what you type.

Terminal window
qualia auth login --base-url https://junction-api.qualiastudios.dev
OptionDescription
--api-key, --tokenAPI key (prompted if omitted)
--base-urlAPI origin, no path. Default https://api.qualiastudios.dev; dev is https://dev-api.qualiastudios.dev
--accountOptional. Account UUID, lowercase; accounts/<uuid> also works. Default: the API key’s own account

Each login replaces the stored key, base URL and account. Leave out --base-url or --account and that value is no longer stored. Where the file lives and how to find your account id: CLI overview.

Show the stored login: whether a key is set (never the key), the base URL, and the account (the API key’s own unless one is stored).

Terminal window
qualia auth status
API key: configured
Base URL: https://junction-api.qualiastudios.dev
Account: the API key's own

It reads the config file and the QUALIA_* environment variables, not the global flags. It exits 1 with Invalid authentication config: … when the stored key or base URL is malformed. It does not call the API, so it does not prove the key works.


Show your current credit balance and how it splits: starter credits, prepaid credits and each voucher, left of granted.

Terminal window
qualia credit
qualia credit --json
Credits: 1700
Starter: 80 of 100
Prepaid: 1,000 of 1,000
Voucher Launch: 620 of 1,000, expires 2026-12-31

The split is for members of the organization. When the key’s creator is no longer a member, only the total prints (null fields in --json).


What people and their keys did: the dashboard’s Activity table. A workspace’s activity is the one --workspace names, else the key’s own; --org reads the whole organization’s, every workspace’s and its own. Reading needs data:read. A key restricted to workspaces cannot read activity.

List activity, newest first.

Terminal window
qualia activity list
qualia activity list --org --filter 'verb = KEEP AND actor = API_KEY' --json
qualia activity list --filter 'create_time >= "2026-10-01T00:00:00Z"' -n 100
OptionDescription
--orgThe organization’s activity, not one workspace’s
-f, --filterAIP-160 comparisons joined by AND: verb, actor (PERSON, API_KEY, …), workspace, text, create_time, subject, action
--rawEvery recorded event (action, summary), not only intentional actions
-n, --limitPage size; at most 100 for activity
--cursorThe nextPageToken of the previous page
--json{"activity": [...], "nextPageToken": ...} ({"events": [...]} with --raw)

The table names who acted where the record has a name, and prints each row’s event id. --json prints full event names.

Show one event as the record holds it. --details adds what it touched (its episodes and their state before) and the fields it changed.

Terminal window
qualia activity show <EVENT_ID>
qualia activity show accounts/<ACCOUNT>/workspaces/<WORKSPACE>/events/<EVENT_ID> --details --json

An id is read through the organization. A full name, which qualia activity list --json prints, works too.

Download the organization’s events record as JSON lines: one event per line, oldest first, every workspace’s and the organization’s own.

Terminal window
qualia activity export -o events.jsonl
qualia activity export -o october.jsonl --start 2026-10-01T00:00:00Z --end 2026-11-01T00:00:00Z
OptionDescription
-o, --outputWhere to write the file (required)
--startEvents created at or after it (RFC 3339, with a timezone)
--endEvents created before it
--json{"path": ..., "events": N}

The file is personal data: member events carry usernames and emails. It needs a key with audit:read, not restricted to workspaces, whose creator is an Admin or Owner. The file appears only once the whole record has arrived; a download that breaks leaves nothing behind.


The organization’s Hugging Face tokens: new runs use the default one.

Every command needs a key with the opt-in integrations:write scope (* does not grant it), made by a Contributor, Admin or Owner of the organization, and not restricted to workspaces. A key’s value goes in once and is never shown again: the list prints its last four characters.

List the organization’s integration keys, newest first.

Terminal window
qualia integration list
qualia integration list --json
ID Provider Name Key Default Added
550e8400-e29b-41d4-a716-446655440000 huggingface ci ...9f3a yes 2026-10-30
660e8400-e29b-41d4-a716-446655440000 huggingface lab ...41bc 2026-10-29

--json prints {"integrations": [{"id", "provider", "name", "last4", "is_default", "created_by", "created_at", "expires_at"}]}. last4 is null for a key shorter than 12 characters.

Add a key. The key is piped in with --secret-stdin, or typed at a prompt that does not echo; on a terminal it always prompts. It is never taken from the command line, where it would stay in your shell history.

Terminal window
qualia integration add huggingface --name lab # prompts for the token
printf %s "$HF_TOKEN" | qualia integration add huggingface --name ci --secret-stdin --json
Argument / OptionDescription
PROVIDERhuggingface
--nameUp to 15 characters (required)
--secret-stdinRead the key from stdin; one trailing newline is dropped
--jsonPrint the key as it is listed

A Hugging Face token starts with hf_. The organization’s first token becomes the default.

Remove a key. Workspaces on a removed token move to the default.

Terminal window
qualia integration remove <ID>
qualia integration remove <ID> --yes --json

Without a terminal it needs --yes.

Make a Hugging Face token the one new runs use.

Terminal window
qualia integration default <ID>

Who is in the organization or a workspace, at which role, and changing it: the dashboard’s Members pages. Without --project a command is about the organization; with it, about that workspace.

A key needs the opt-in members:read scope to read and members:write to change (see Admin scopes); reading a space also takes projects:read. A key acts as the person who made it: it never gives a role above that person’s own, and only an admin or owner manages people. A workspace key acts only on its own workspace, so it always passes --project.

List everyone with access, their role and where it comes from: granted here, from the organization, or through a space’s members.

Terminal window
qualia member list
qualia member list --project <PROJECT_ID> --json
OptionDescription
-p, --projectA workspace; without it, the organization
--json{"space", "people": [...], "can_manage_members", "my_role"} as served

Show one person’s role on every space of the organization you can read, and the role granted on each. Needs an organization key.

Terminal window
qualia member show <USER_ID>

Give a person a role, or move them to another: on the organization, or on one workspace with --project. An organization role is for the organization’s members; invite anyone else. A workspace role raises a member above their organization role there, or makes someone a guest of it.

Terminal window
qualia member role <USER_ID> contributor
qualia member role <USER_ID> admin --project <PROJECT_ID> --json
Argument / OptionDescription
USER_IDThe person (required)
ROLEviewer, contributor, admin or owner
-p, --projectA workspace; without it, the organization
--jsonThe role before (from_role) and after, and the role they hold now

Take a person’s role on one workspace away (--project; they keep what the organization gives them), or remove them from the organization. --keep names the workspaces they stay a guest of. A key cannot remove the person who made it from the organization: leaving one takes a signed-in session.

Terminal window
qualia member remove <USER_ID> --project <PROJECT_ID> --yes
qualia member remove <USER_ID> --keep <PROJECT_ID> --yes
OptionDescription
-p, --projectA workspace; without it, the organization
--keepRemoving from the organization: a workspace they stay a guest of (repeatable)
-y, --yesSkip the confirmation (needed without a terminal)
--jsonWhat they hold now (effective_role), and the workspaces kept

Invitations to the organization or a workspace. The same scopes and rules as qualia member.

List the pending invitations: to one workspace with --project, or every one in the organization, each naming its space.

Terminal window
qualia invitation list
qualia invitation list --project <PROJECT_ID> --json

Invite someone by email at a role: to the organization, or to one workspace as a guest with --project. Never above your own role. Someone already in the organization gets a role with qualia member role instead.

Terminal window
qualia invitation send new@example.com --role viewer
qualia invitation send new@example.com --role contributor --project <PROJECT_ID>
Argument / OptionDescription
EMAILTheir email address (required)
-r, --roleviewer, contributor, admin or owner
-p, --projectA workspace; without it, the organization
--jsonThe invitation, with its id

Withdraw a pending invitation. Without --project it is found anywhere in the organization; a workspace key passes its own workspace.

Terminal window
qualia invitation withdraw <INVITATION_ID> --yes

Wherever a command takes a dataset (DATASET or --dataset/-d, in qualia check, qualia mixture and qualia task link too), name it by its id, its full name (accounts/<account>/datasets/<id>) or its slug: the last part of its dashboard address, /datasets/<slug>, and slug in qualia dataset list --json. The examples below write <DATASET_UUID>; a slug works in its place. A slug costs one extra request, and one no dataset has is refused with a pointer to qualia dataset list.

The short id in the Dataset ID column of qualia dataset list, the first 8 characters of the id, names a dataset too, and so does any longer start of the id, while exactly one dataset of the workspace begins with it. If several do, or it is also another dataset’s slug, the command lists them and stops. A short id is found by reading the workspace’s dataset list, one request for each 500 datasets, and one more for the slug it may be, so a script that names a dataset often should pass the slug or the whole id.

Two commands take the full id only: qualia dataset delete (or the full name) and qualia dataset define --dataset-id.

List the datasets in your account, by name: uploads and merged datasets, or the kind you ask for. It reads the qualia.v1 Datasets API in the API key’s own account. An older qualia-sdk with no account set falls back to the older list route (uploads only).

Terminal window
qualia dataset list
qualia dataset list --search cube --dataset-type lerobot_v3
qualia dataset list --method umi --method teleop
qualia dataset list --tag gello --tag warehouse-a
qualia dataset list --kind mixture
qualia dataset list --kind all --filter 'merged_dataset = "accounts/<a>/datasets/<id>"'
qualia dataset list --limit 50 --json
OptionDescription
--limit, -nPage size, 1–100 (default 20)
--cursorPagination cursor printed by the previous page
--search, -qOnly names containing this text
--dataset-typeFilter: lerobot_v2_1, lerobot_v3, or mcap_ros2
--methodAny canonical tag; repeat to match alternatives
--tagAny collection tag; repeat to match alternatives
--kindupload, merged, mixture or all (default: uploads and merged)
--filterAn AIP-160 filter, ANDed on (state, labels.*, target_robot, …)

--filter 'merged_dataset = "<name>"' lists the uploads a merged dataset holds, which the list otherwise leaves out.

--dataset-type filters each page after it arrives, so a page can be shorter than --limit; follow the printed cursor to read on.

The Name column names each dataset as the dashboard does; a long name continues on the next line. On the older route (an old qualia-sdk) there are no names, so it shows the repository’s. --json also gives the repository (acct-<uuid>/<name>).

The Dataset ID column shows the first 8 characters of each id. Every dataset command takes that short id, as it takes the whole id, the full name and the slug that --json prints (see qualia dataset).

The Verdict column says what each upload amounted to: published, degraded (episodes were derived, but part of the upload was flagged) or underivable (no episode could be derived; the raw bytes are kept). Datasets published before verdicts existed show published. --json prints what the call returned: qualia.v1 Datasets with verdict and verdict_reason; on the older route, the older items with qds.ingest_verdict and qds.verdict_reason.

Edit this dataset’s collection metadata and custom tags. The recognized names are teleop, vr, umi, egocentric, policy-rollout, sim, real-world, human and gello; your own tags are accepted too. The set holds at most 64 distinct tags. A merged dataset changes only its own set, and an append preserves it. Version tags and episode curation tags are separate.

Terminal window
qualia dataset tags <dataset-uuid> --tag umi --tag real-world --tag human --tag gello
qualia dataset tags <dataset-uuid> --tag warehouse-a
qualia dataset tags <dataset-uuid> --clear

A changed, nonempty classification set updates saved metadata and customs in one atomic write. Custom-only input, or unchanged classifications with edited customs, replaces only customs and preserves declarations and inference. When classifications are present, echo them to remove customs without declaring inferred methods. --clear clears customs and explicitly makes every collection dimension Unknown, so inference cannot refill it. Omitting both --tag and --clear is an error. The command needs an account-scoped API connection and independent collection metadata support. The SDK reads the owner and uses its etag unless --etag was supplied; a stale caller etag is refused.

Replace the six-name compatibility view on an upload or merged dataset. A merged dataset changes only its own set. Other collection dimensions and free tags remain; use qualia dataset tags for the other dimensions and customs. The six tags are teleop, vr, umi, egocentric, policy-rollout and sim. Use --clear to explicitly record an empty set (Unknown); omitting all --method options is an error.

Terminal window
qualia dataset methods <dataset-uuid> --method umi --method teleop
qualia dataset methods <dataset-uuid> --clear

Show the account’s in-flight uploads, the feed the dashboard’s pending band renders, from any machine, including one that is not running the upload.

Terminal window
qualia dataset uploads
qualia dataset uploads --watch --every 30
qualia dataset uploads --json
OptionDescription
--watch, -wRefresh until Ctrl-C
--everySeconds between refreshes (default 15, never under 5)

Files and Bytes count content the lake has already accepted. Bytes go straight to storage, so a first upload of new content reads 0 until it publishes; Phase and Detail track the run throughout. Stop one with cancel-upload, and clear a failed one with dismiss-upload.


Publish a local dataset directory as a pinned DatasetVersion. Bytes go straight to storage; Qualia verifies the manifest and publishes the two together. Episodes become visible only once that publication succeeds.

Terminal window
qualia dataset upload ./my-recording my-dataset
qualia dataset upload ./teleop-session teleop-1026 --source-format mcap_ros2
qualia dataset upload ./umi-session umi-1026 --source-format mcap_ros2 --rig "Kitchen UMI"

It prints the DatasetVersion id it published and the version a download takes, v0 for a fresh upload: Download it with: qualia dataset download my-dataset:v0 (reference in --json).

Where uploads are prepared, the receipt then names the next step in a line that starts Next:. Preparation finds the tasks in the upload and links its episodes to them: wait for it and read what it found with qualia dataset preparation <DATASET_UUID> --wait (next_step in --json, with its command and message). Until preparation has ended, qualia check estimate answers that no episode is linked. The wait hint also appears when automatic paid video checks are off or their policy cannot be read.

Where uploads are prepared and automatic paid video checks are enabled, the receipt says the VLM check may run after preparation if the setting remains on. Its bound is set only once preparation has linked the takes to Tasks, so the receipt names qualia check estimate <DATASET_UUID> rather than a number (automatic_check in --json, with credits null). With video checks off, no preparation or an unreadable policy, the line is left out. Automatic-check eligibility is saved with the published upload; a later opt-in does not enable paid checks for that upload. Before a paid run starts, the current policy is checked again, so a later opt-out prevents its admission. Change the setting with qualia check policy-set.

--source-format takes lerobot_v2_1, lerobot_v3, or mcap_ros2. Leave it out and the format is read from the directory: codebase_version in meta/info.json tells LeRobot 2.1 from v3, and any .mcap makes it a session. LeRobot 2.1 is never aliased or converted to v3, so its source provenance stays distinct.

Use --source-format mcap_ros2 for a ROS 2 bag session. Lay it out as one subdirectory per recording, each holding its .mcap bags and, optionally, an episode.json sidecar carrying task and mode fallbacks. Every .mcap is ingested on its own, so rosbag2 split files (bag_0.mcap, bag_1.mcap, …) are accepted; see ROS 2 MCAP.

An MCAP session may carry meta/ingest.toml beside its bags, at the root of the directory you upload, to declare its state and action topics, its clock and its episode boundaries. Most teleoperated arms need none. See Declaring state and action channels and Declaring episode boundaries.

Use --rig for a handheld (UMI) recording. It names the UMI rig the recording was made on: its name or id (the old name umi-handheld is the rig that holds that key, else the workspace’s one UMI rig). A recording whose meta/ingest.toml declares [rig] kind = "umi-handheld" and names no rig takes the workspace’s one UMI rig; none or several refuses it. A workspace has the UMI rigs someone made and none otherwise, so make one first with New rig, UMI in the dashboard or qualia gripper umi-rig and calibrate it; naming one that does not exist is refused before anything is stored. The server writes that rig’s kind and TCP calibration into the upload’s meta/ingest.toml, so the recording converts to gripper poses as it lands. Your local file is never written. See Handheld (UMI) recordings.

Each upload is its own dataset unless you say otherwise. --merge-target appends this one upload to an existing merged dataset; pass its id, its slug or its full resource name (accounts/{account}/datasets/{id}). A merged dataset is made with qualia dataset merge and is listed as merged by qualia dataset list. To append every upload to the same merged dataset, save it once with qualia dataset destination set; each upload then prints Upload destination: append to … on stderr before it starts, and Appended upload to … once it has published (merge_append in --json). --standalone keeps one upload on its own even when a destination is saved. --merge-target and --standalone cannot be combined.

One dataset that grows with every batch takes three steps. A merge takes at least two uploads, so on day one there is nothing to merge yet:

Terminal window
qualia dataset upload ./day-1 day-1 --standalone # day one
qualia dataset upload ./day-2 day-2 --standalone # day two, then merge the two once
qualia dataset merge day-1 day-2 --name "Bench" # final; try it with --dry-run first
qualia dataset upload ./day-3 day-3 --merge-target <MERGED_DATASET> # from then on
OptionDefaultDescription
--source-formatdetectedSource layout: lerobot_v2_1, lerobot_v3 or mcap_ros2; detected from the directory when omitted
--upload-workershalf your cores, 4–8Large (over 64 MiB) or edited files read and uploaded at once
--chunk-threads≈ half your coresThreads used to hash and split files
--quietoffSilence the live per-phase progress on stderr
--merge-targetsaved destinationAppend this upload to this merged dataset (id, full name or slug), for this run only
--standaloneoffKeep this upload its own dataset even when a destination is saved
--rignoneConvert a handheld (UMI) recording at this UMI rig as it lands: its name or id (the old name umi-handheld is the rig that holds that key, else the workspace’s one UMI rig)
--allow-underivableoffUpload an MCAP session even when no bag in it can be derived; a LeRobot structure refusal cannot be lifted
--jsonoffPrint the receipt as JSON on stdout (see the exit codes below)

Before a byte moves, an mcap_ros2 upload checks every bag for damage the server can never derive episodes from: a file too small to be MCAP, a bad leading magic, a cut before its first complete data record, or a record whose framing runs past the file. A well-formed bag with no messages is treated the same way. A damaged or empty bag does not stop the upload: it is listed, with the byte offset where it is known, and uploads as recorded; ingest reports it as source_underivable and the rest of the session publishes.

A bag cut after the recording started is listed as source_truncated when the part before the cut holds robot state, and the server derives the part that was recorded. By default one JointState message is enough. When meta/ingest.toml declares [state].topics, every declared topic needs a message before the cut; one missing topic makes the bag source_underivable (no_state), as does a cut bag with camera frames but no state. When the client cannot open the bag’s compressed chunks (lz4, zstd before Python 3.14, or a chunk too large to inspect), or cannot parse meta/ingest.toml, it lists the bag as source_truncated and the server decides; the server may still find no state and report no_state.

Only a session the server can derive nothing from, with no .mcap at all or every bag damaged or empty, stops the upload with exit code 1:

BLOCKED — 2 file(s) the server cannot derive; nothing was uploaded:
./session/episode-0/recording.mcap: truncated MCAP file (missing the trailing magic) — the recording did not finish [source_underivable]
./session/episode-1/recording.mcap: truncated MCAP file (missing the trailing magic) — the recording did not finish [source_underivable]
./session: every bag is underivable — no episode can be derived [session_no_episodes]
Re-run with --allow-underivable to upload the bytes as recorded.

With --allow-underivable the bytes upload anyway and are kept as a source dataset with no episodes. The receipt (--json) carries the check as preflight.

A LeRobot upload (lerobot_v2_1 or lerobot_v3) is checked the same way. The server refuses some structure outright, so these stop the upload with exit code 1, all listed in one run:

CheckCode
meta/info.json missing or not a JSON object; fps not in (0, 1000]; total_tasks not a positive integer; no features object; a video_path placeholder the server cannot render; lerobot_v2_1 without a positive chunks_sizeinfo_json_unreadable
codebase_version is not v2.1 for lerobot_v2_1 or v3.0 for lerobot_v3codebase_version_mismatch
no *.parquet file under data/data_shards_missing
lerobot_v2_1 without meta/episodes.jsonl or meta/tasks.jsonlinfo_json_unreadable

--allow-underivable cannot override these: the server refuses the structure whatever the flag says, so the upload stops with exit code 1 either way.

A camera whose video rate differs from the state and action row rate is accepted and flagged as video_rate_differs_from_rows; the viewer aligns them by time, but LeRobot download and fine-tuning require re-exporting the video at the row rate and uploading again.

A missing video does not stop the upload, and nothing is repaired for you. Paths come from video_path in meta/info.json, or the server’s default videos/{video_key}/chunk-{chunk_index:03d}/file-{file_index:03d}.mp4 when it has none. For lerobot_v2_1 every episode’s video is checked and each missing one is listed with its episode and camera; for lerobot_v3 each camera needs at least one file matching the template, and a camera with none is listed as missing from every episode. Both use camera_absent_for_episode: the upload continues, those episodes will be flagged, and you should probably exclude them. With no video for any camera, one warning says every episode will be flagged.

Every completed upload ends with a verdict, ingest_verdict in the --json receipt (with verdict_reason and verdict_source), and an exit code a script can branch on:

ExitVerdictMeaning
0published or degradedThe dataset published; degraded means an ingest finding, a flagged episode or an unreadable ingest report
1noneIt failed or was refused: a pre-flight block or an error
3underivableThe bytes are stored as a source dataset, but no episode could be derived

An underivable upload ends with one line on stderr naming the dataset that keeps the bytes and the reason:

UNDERIVABLE — no episodes could be derived; the bytes are kept as dataset <ID>. Reason: session_no_episodes: …

With --json, stdout carries the full receipt for a completed upload (exit 0 or 3) and {"preflight": …} for a pre-flight block (exit 1). Any other error prints nothing on stdout and exits 1 with its message on stderr.

Exit 1 does not mean nothing is left. A pre-flight block moves nothing, but after a transfer began the upload stays in qualia dataset uploads as failed, with its bytes.

Progress is written to stderr, so --json output stays pipeable. The server-side phases are bounded by silence rather than elapsed time: ingest parses every bag and transcodes every camera, so a large upload is free to take as long as it takes. See Configuration for the two environment variables that govern that wait.


A destination is a merged dataset that later uploads append to by default, so you do not pass --merge-target every time. It is saved in this CLI’s config file, bound to the API host, account and workspace it was saved under. An upload made under another host, account or workspace ignores it, stays standalone and says so on stderr.

Terminal window
qualia dataset destination set accounts/<account>/datasets/<id>
qualia dataset destination show
qualia dataset destination clear
CommandWhat it does
setSave TARGET, an existing merged dataset (its id, full name or slug), as the default --merge-target for later uploads
showPrint the saved destination with its API host, account and workspace; --json prints them as JSON
clearRemove the saved destination; later uploads are standalone again

set refuses a dataset that is not merged. A single upload can still go elsewhere with --merge-target, or stay on its own with --standalone.


Show which cameras a source dataset publishes and every degradation the ingest performed instead of failing. A camera missing from one bag is dropped from the whole dataset, so this is where a two-camera pin of a three-camera recording is explained. It opens with the upload’s report card: how many episodes were accepted, and which were flagged and why.

Terminal window
qualia dataset ingest-report <DATASET>
qualia dataset ingest-report <DATASET> --json
qualia dataset ingest-report acct-<ACCOUNT_UUID>/<NAME> --ref <COMMIT>
Argument / optionDescription
DATASETDataset id, full name or slug; or a canonical path acct-<uuid>/<name>
--refWith a path only: branch, tag or commit to read the report from (default: tip)

By id, name or slug the report is read through the qualia.v1 Datasets API; a merged dataset answers its uploads’ reports folded into one, and --json prints that API’s IngestReport. A path reads the older route. A report too large to read inline is an error, never shown as clean.


Download a dataset version in a format, or every file uploaded to the dataset, byte for byte (--format original).

Terminal window
# the newest version (stable, or v0 when nothing is tagged yet), as LeRobot v3
qualia dataset download my-dataset
# one version, in each format
qualia dataset download my-dataset:1.0
qualia dataset download my-dataset:1.0 --format lerobot_v2_1
qualia dataset download my-dataset:1.0 --format mcap_ros2
# files to upload to Physical Intelligence or Dream Labs
qualia dataset download my-dataset:1.0 --format pi_mcap_v1
qualia dataset download my-dataset:1.0 --format dreamlabs_v2_1
# every file uploaded, byte for byte
qualia dataset download my-dataset --format original

DATASET is the dataset’s slug, id or short id; add :<version> for a tag, stable (the newest tag), v0 (an upload as it arrived), a DatasetVersion id such as the one qualia dataset upload prints, or a prefix of a digest or id. A full accounts/<account>/datasets/<dataset>/versions/<tag> name also works. Without a version you get the newest one: stable, or v0 for an upload with no tag yet, and the command says which it took.

DEST defaults to ./<slug>-<tag>, with -v21 for LeRobot v2.1, -mcap for MCAP, -pi for pi_mcap_v1 and -dreamlabs for dreamlabs_v2_1, and to ./<slug>-original for original.

Argument / optionDescription
DATASET<dataset>[:<version>], or a full version name
DESTDestination folder (optional)
--formatlerobot_v3 (default), lerobot_v2_1, mcap_ros2, pi_mcap_v1, dreamlabs_v2_1 or original; dashes work too (mcap-ros2)
--jsonPrint the result as JSON

pi_mcap_v1 and dreamlabs_v2_1 are files to upload to Physical Intelligence or Dream Labs with your own account: the version’s episodes that are not rejected (Dream Labs: only the successful ones), with a README.md that says how to upload them (and upload_to_pi.py for Pi). An episode that could not be converted is listed there with its error. See Partner layouts.

original is every file uploaded to the dataset, byte for byte, with no version involved. It also holds what ingest stored beside those files: the annotations under meta/qds, meta/ingest_report.json and, for an MCAP upload, the LeRobot data and videos ingest made from the recordings. Trims and splits are not applied: the takes are whole, and a split’s pieces are only annotations under meta/qds. When the dataset has any, the command says how many on stderr and how to get the curated episodes (tag a version, then download it). Only an upload has files of its own, so a merged dataset or a mixture is refused: download one of its uploads, or a version. For now original needs an organization key; a workspace key is refused.

An MCAP download keeps only what was mapped at upload: tf, diagnostics and custom topics do not come back, so use original when you need them.

While the server builds a version (or, for original, lists the files) you see a spinner on stderr, then a bar with bytes, speed and files n/m. Files are checked as they arrive, and a run that stops keeps the files it checked: run the same command again and it fetches only the rest. Nothing is drawn with --json.

It ends with Saved <version> as <format> to <DEST> (<N> files, <size>), plus ; <K> kept from an earlier run when files were reused.

--json prints destination, format, reference, version, dataset_version_id, file_count, byte_count, downloaded_file_count, reused_file_count and materialization (the qualia.v1 Materialization). For original, reference is the dataset’s slug and version, dataset_version_id and materialization are null.

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 of it in that format need read access only (data:read). head, what a dataset yields right now, has no download: tag it first.

A version that contains a blocked episode (a NaN, an infinity, a missing value or a wrong width in its state or action rows) is refused, because a download carries every episode. Publish a curated version without them first, with qualia dataset curate <DATASET_VERSION_UUID> --idempotency-key <UUID> --name <NEW_DATASET>, and download that. --format original is not refused: it renders nothing, so it downloads the upload’s files, blocked episodes included.

The dashboard gives the command with the version filled in: Download on a dataset or version page.


Delete a mixture, or retire an upload or a merged dataset, for example after uploading the wrong folder. It leaves every list and every mixture, versions cut from it keep resolving, and no storage is reclaimed. A merged dataset is retired with the uploads it holds; the prompt says how many.

Terminal window
qualia dataset delete <FULL_DATASET_UUID>
qualia dataset delete accounts/<account>/datasets/<id> --force --yes
qualia dataset delete <FULL_DEFINITION_UUID> --definition --yes

The id must be the full UUID or the full name: qualia dataset list --json shows it as name (accounts/<account>/datasets/<id>). It is refused while a mixture reads the dataset, and while Tasks reference its episodes unless --force. On the older route (an old qualia-sdk) it retires uploads only.

OptionDescription
--forceAlso remove the Task references to its episodes (the Tasks stay)
--etagRefuse if the dataset changed since this etag
--definitionDelete a dataset definition instead; datasets are untouched
--yes, -ySkip the confirmation (required without a terminal)
--jsonPrint name, displayName, kind and deleted

Import one exact Hugging Face dataset commit server-side. The command confirms the pinned coordinates, submits them once, and polls while Hub bytes stream straight into storage. Success creates a pinned immutable DatasetVersion.

Terminal window
export HF_TOKEN="..." # optional for public datasets
qualia dataset import-hf \
qualia-robotics/openarm-cube-in-box-v1 \
17c29c487d886878ddcb3c5649101b2860fbc047 \
cube-in-box \
--source-format lerobot_v3

The revision must be lowercase 40-hex; branches, tags, URLs, and embedded credentials are rejected. HF_TOKEN is read only from the environment and is never accepted as a command-line option, printed, or sent in poll requests. Without it, a private or gated repository is read with the workspace’s Hugging Face key from Settings → Integrations (qualia integration add huggingface); with no key that can read it, the import fails and says which key to add. Local file/byte/work-directory switches do not exist because no dataset body or scratch file touches the CLI machine. --json requires --yes so stdout stays valid JSON.

Save a lake query as a dataset definition, a target curate can publish a version into. qualia dataset pin <DEFINITION_UUID> freezes what it selects as an immutable version.

Terminal window
qualia dataset define kitchen-2026-09-16 --dataset-id <SOURCE_DATASET_UUID>
qualia dataset define audited-only --query ./query.json --description "human audited"
Argument / optionDescription
NAMEOne path segment: letters, digits, ., _, - (required)
--dataset-idSelect episodes of this source dataset; repeatable
--taskSelect one task
--tagRequire this tag; repeatable
--queryJSON file holding the complete saved query, instead of the flags
--descriptionFree text

--query takes the shape of the SDK’s DatasetSavedQuery: dataset_ids, task, tier, min_duration_s, max_duration_s, tags, review_state, search, numeric_predicates, predicates, episodes, query_text.


Freeze something as an immutable DatasetVersion. The printed DatasetVersion id is what training-features and curate take, and what the dashboard’s training launcher trains on.

  • pin DATASET TAG publishes a version of an upload or a merged dataset: the takes kept by a person (a key’s keeps count), with the trims and splits saved on them. It first freezes and measures that selection, as version-preview does, and refuses to publish while it leaves a take out, unless --allow-left-out. A take that is only kept automatically is not taken: --include kept takes it. --as-recorded and --digest tag head instead: every kept episode as recorded, with saved trims and splits left out. A mixture always tags head. Tags never move; a tag name is taken once per dataset. Acts in the API key’s own account (--account is optional).
  • pin DEFINITION_ID freezes what a definition’s saved query selects now. This stays on the /v1 path, because saved queries have no qualia.v1 equivalent.
Terminal window
qualia dataset pin <DATASET_UUID> 1.0 --note "first cut"
qualia dataset pin <DATASET_UUID> 1.0 --preview <PREVIEW_NAME>
qualia dataset pin <DATASET_UUID> 1.1 --include kept --allow-left-out
qualia dataset pin <DATASET_UUID> recorded-1 --as-recorded
qualia dataset pin <DATASET_UUID> 1.2 --digest <HEAD_DIGEST_YOU_REVIEWED>
qualia dataset pin <DEFINITION_UUID> --note "first pin"
Argument / optionDescription
TARGETWith TAG: dataset id, full name or slug. Without: a definition UUID
TAGTag name, e.g. 1.0: 1-64 of [0-9A-Za-z._-]
--noteWhat the version is
--includeWith TAG: which takes, by how they stand: kept_by_person (default), kept, flagged, rejected, rejected_by_person; repeatable
--previewWith TAG: publish this frozen preview, by the name or id version-preview printed
--as-recordedWith TAG: tag head, leaving saved trims and splits out
--digestWith TAG: tag head while it has the digest you reviewed; refused (409) if head moved
--allow-left-outWith TAG: publish although the preview leaves takes out
--jsonJSON output

--include, --preview, --as-recorded and --digest are mutually exclusive. --allow-left-out goes with neither --as-recorded nor --digest, and none of these goes with a definition.


Freeze and measure what a version would hold, and publish nothing: the takes picked by how they stand, each with the trim or split saved on it. It prints the preview’s name, its episodes and duration, the takes it leaves out with why, every window, and a summary of the measured metrics. Publish exactly that preview with qualia dataset pin DATASET TAG --preview. An upload and a merged dataset have previews; a mixture tags head.

Terminal window
qualia dataset version-preview <DATASET_UUID>
qualia dataset version-preview my-dataset --include kept --include flagged
qualia dataset version-preview my-dataset --json > preview.json # .preview.name names it
Argument / optionDescription
DATASETDataset id, full name or slug
--includeWhich takes, by how they stand: kept_by_person (default), kept, flagged, rejected, rejected_by_person; repeatable
--json{"preview": {...}, "items": [...], "metrics": {...}}: the preview, every window and the metric summaries

A window names its take in take. Its label counts takes from 1, so episode 8 is the take whose index is 7. Each metric’s count, mean, p5, p50 and p95 are over the measured windows only. You have one live preview of a dataset: a new one replaces the last.


List a dataset’s tags, newest first.

Terminal window
qualia dataset version list my-dataset
qualia dataset version list my-dataset --json
Argument / optionDescription
DATASETDataset id, full name or slug
--filterAIP-160 filter over digest and create_time
--limit, -nPage size, 1-500 (default 50)
--cursorNext-page token
--json{"versions": [...], "nextPageToken": ...}

Show one version: its tag, reference, digest, episode counts, DatasetVersion id and note.

Terminal window
qualia dataset version get my-dataset:1.0
qualia dataset version get stable --dataset my-dataset --json

VERSION is <dataset>:<tag> (slug or UUID; a tag, stable, head or a digest prefix), a full version name, or a tag with --dataset.


What came in, went out or changed revision between two refs, and why. It also prints the note and the tag the dashboard’s tag dialog would offer.

Terminal window
qualia dataset version diff my-dataset # newest tag → head
qualia dataset version diff my-dataset --base 1.0 --target 1.1 --json
OptionDescription
--baseA tag, stable or a digest prefix; default the newest tag
--targetDefault head
--limit, -nChanged episodes per page, 1-1000 (default 100)
--cursorNext-page token

Publish a version. It does what qualia dataset pin DATASET TAG does, with the same options, and also takes the dataset’s slug: by default the takes kept by a person, with the trims and splits saved on them; --as-recorded or --digest to tag head. A mixture always tags head. The tag never moves.

Terminal window
qualia dataset version create my-dataset 1.0 --note "first cut"
qualia dataset version diff my-mixture --json # read targetDigest
qualia dataset version create my-mixture 1.1 --note "dropped 3 jerky takes" --digest b3:…

--digest is the head digest you reviewed; if head has moved since, the tag is refused (409).


Edit a version’s note, the one thing about a tag that changes.

Terminal window
qualia dataset version note my-dataset:1.0 "first cut, 120 takes"

--etag refuses the edit if the note changed since you read it.


A merged dataset’s merges, newest first, and the uploads each took. A merge is final: none can be reverted.

Terminal window
qualia dataset version merges my-merged-dataset --json

What was done to the dataset in the round a version opened: from its creation to the next tag’s (for head, since the newest tag). It lists rulings, edits, splits and issues, each with who did it and when, as the dashboard’s Curation of 1.0 panel does. A merged dataset’s record also reads the uploads it holds. At most 2000 events are read; a longer round shows its newest.

Terminal window
qualia dataset version record my-dataset:1.0
qualia dataset version record my-dataset:head --json

It reads the events of the dataset’s workspace.


Explain bad-data recommendations for immutable DatasetVersions without mutating them. Use --page-token to continue the result set.

Terminal window
qualia dataset curation-suggest <DATASET_VERSION_UUID>
qualia dataset curation-suggest <DATASET_VERSION_UUID> --json

Publish the reviewed analysis’s sealed recommendation set as a new immutable DatasetVersion. Supply a stable idempotency UUID. Source bytes and Git history are not deleted. Retrying with the same idempotency key resumes or recovers that publication’s receipt instead of suggesting fresh evidence against a head the first attempt already moved. A failed publication keeps its key: publish again under a new one.

Terminal window
qualia dataset curate <DATASET_VERSION_UUID> \
--definition-id <DATASET_DEFINITION_UUID> \
--idempotency-key <OPERATION_UUID> \
--yes

Omit --yes for an interactive confirmation.

Pass --name <NEW_DATASET> instead of --definition-id to publish the curated version as a new dataset. That is the form to use on an upload: an upload’s own dataset is source-owned and refuses a curated version. The curated version leaves out every episode the analysis recommends removing, not only blocked ones.


Show the exact camera, stream, timebase, and frame-timestamp descriptors for one immutable DatasetVersion. VERSION is <dataset>:<tag>, a DatasetVersion UUID, or a full version name. The output includes the resolved id; head must be pinned first.

Terminal window
qualia dataset training-features my-dataset:1.0
qualia dataset training-features my-dataset:1.0 --json
ArgumentDescription
VERSION<dataset>:<tag> (slug or dataset UUID), DatasetVersion UUID, or full version name (required)

Show the recorder markers inside one episode, in time order, in seconds from the start of the recording. Every edit command below cuts on these labels, so this is how you find out what a recording actually wrote.

The episode commands below speak qualia.v1, in the API key’s own account, and they name episodes through the upload they were recorded in: --dataset/-d with bare episode ids, or full names accounts/…/datasets/…/episodes/….

Terminal window
qualia dataset markers <EPISODE_UUID> -d <DATASET_UUID>
qualia dataset markers <EPISODE_UUID> -d <DATASET_UUID> --json

Cut episodes into children at their own markers, at given instants, or around windows to keep. Children are new episodes and the parents are left as they were, but a split is published when it answers and cannot be undone: dataset unsplit removes only children that were never published. Preview with --dry-run; once split, curate a child by trimming or rejecting it.

Terminal window
qualia dataset split <EPISODE_UUID> -d <DATASET_UUID> --cut-at grasp
qualia dataset split <EPISODE_UUID> <EPISODE_UUID> -d <DATASET_UUID> --cut-at grasp --dry-run
qualia dataset split <EPISODE_UUID> -d <DATASET_UUID> --at 12.5 --at 31
qualia dataset split <EPISODE_UUID> -d <DATASET_UUID> --keep 0.5-6 --keep 8-14 --dry-run
OptionDescription
--dataset, -dThe upload the episodes were recorded in (required)
--cut-atMarker label to cut at, as the recording wrote it
--atCut at this instant, in seconds from the start of the recording
--keepKeep this window as one child, START-END seconds, e.g. 1.5-9
--start-at-firstDrop the lead-in, up to the FIRST marker so labelled
--end-at-lastDrop the run-out, after the LAST marker so labelled
--dry-runPlan the cuts without writing them
--jsonJSON output

A request cuts one way: at markers, at instants or around windows. For markers, name at least one label across --cut-at, --start-at-first or --end-at-last. The narrowing options apply before the cut, which is how a lead-in is dropped; given alone, each episode becomes one trimmed child. All three accept repeats.

--at is for takes the recorder marked nothing at. Repeat it for several cuts; every named episode is cut at the same instants. It cannot be combined with the marker options. To find the instants, watch the take (dataset videos) or read it with the SDK’s episodes.sample_series.

--keep makes one child of each window and drops what lies outside every window: the lead-in, the run-out and the stretches between. Repeat it for several windows, in time order and not overlapping; every named episode keeps the same windows. It cannot be combined with --at or the marker options. The summary counts the frames no child keeps.

A trimmed episode is cut inside its trim: its children keep only the frames the trim keeps, what the trim cut is counted as dropped, and a marker or instant there cuts nothing.

An episode with no matching cut, or with children already, is left alone.


Delete the children of split episodes that were never published: those a split made before splits published. A published split is refused; curate its children by trimming or rejecting them.

Terminal window
qualia dataset unsplit <PARENT_EPISODE_UUID> -d <DATASET_UUID>

Trim episodes to their own markers, or to given seconds, reversibly. Each episode is narrowed to start at ITS first named marker and end at ITS last. The base extent never changes, so --clear restores it.

Terminal window
qualia dataset trim <EPISODE_UUID> -d <DATASET_UUID> --start-at-first ready --end-at-last done
qualia dataset trim <EPISODE_UUID> -d <DATASET_UUID> --start 1.5 --end 12
qualia dataset trim <EPISODE_UUID> -d <DATASET_UUID> --clear
OptionDescription
--dataset, -dThe upload the episodes were recorded in
--start-at-firstTrim the lead up to the FIRST marker so labelled
--end-at-lastTrim the tail after the LAST marker so labelled
--startStart each episode here, seconds from the start of the recording
--endEnd each episode here, seconds from the start of the recording
--clearRestore the full extent instead of trimming
--jsonJSON output

Use one way per call: marker labels, --start/--end, or --clear. --start or --end alone moves that end and keeps the episode’s other one (the whole recording when it was untrimmed). Each episode is read first and written with the etag it read, so an edit made in between is refused rather than overwritten. An episode the server will not trim, such as a split parent, is left alone with its reason.


Name each episode’s subtask spans from its own markers — the same boundary rule dataset split cuts on, used to label a span instead of carving it, so a whole selection can be named at once.

Terminal window
qualia dataset subtasks <EPISODE_UUID> -d <DATASET_UUID> --at grasp --at place
qualia dataset subtasks <EPISODE_UUID> -d <DATASET_UUID> --at grasp --starts-at
OptionDescription
--dataset, -dThe upload the episodes were recorded in (required)
--atMarker label whose presses bound a subtask (required, repeats)
--starts-atEach marker OPENS the span it names (default: it closes it)

Edit many episodes in one call: all of them change or none do, one revision each. Name the episodes and the fields they all take, or pass --from with a JSON list of per-episode edits (episode plus the fields to change, and an etag to refuse the call if that episode changed since you read it). Up to 500 episodes of one upload per call. Trim with dataset trim and link tasks with dataset assign-task or dataset link-episode.

Terminal window
qualia dataset edit <EPISODE_UUID>... -d <DATASET_UUID> --tag slow --outcome FAILURE
qualia dataset edit -d <DATASET_UUID> --from edits.json
OptionDescription
--dataset, -dThe dataset the episodes are read through (required)
--taskTask label
--instructionInstruction
--sceneScene
--tagReplace the tags with these (repeats)
--clear-tagsRemove every tag
--outcomeSUCCESS, PARTIAL, FAILURE or OUTCOME_UNSPECIFIED
--fromJSON list of per-episode edits; - reads stdin

Label episodes with a task and link them to it. In an upload it changes all of them or none. On a merged dataset it is all or none per upload: each upload commits on its own, so a failed call can leave earlier uploads changed. Rerun it to finish. With --task, every selected episode links to that Task and, without --label, takes its display name as the label: the language instruction training reads. A label alone sets the text and leaves links as they are.

Use it when the episodes need the label as well as the link. To only say which Task an upload was recorded against, use link-task.

Terminal window
qualia dataset assign-task -d <DATASET_UUID> --task <TASK_UUID> --filter 'state = KEPT'
qualia dataset assign-task <EPISODE_UUID> -d <DATASET_UUID> --label "restock shelf"
OptionDescription
--dataset, -dThe upload the episodes were recorded in (required)
--task, -tTask to link: id or accounts/.../tasks/...
--labelThe label; defaults to the task’s display name
--filterSelect episodes by filter instead of naming them
--jsonJSON output

Pass --task, --label or both. Name episodes or pass --filter, not both.


Set the Task an upload was recorded against. Every episode with no link of its own reads it, in one request. On a merged dataset it is set on every upload the dataset holds. A link, not a label: labels.task stays as it is, and a mixture can follow every upload linked to the task.

Episodes that link the old Task themselves keep it. Pass --relink to move them too.

Terminal window
qualia dataset link-task <DATASET_UUID> <TASK_UUID>
qualia dataset link-task <DATASET_UUID> <TASK_UUID> --relink
qualia dataset link-task <DATASET_UUID> --unlink
OptionDescription
--unlinkRemove the upload’s task link (omit the task)
--relinkAlso move the episodes linked to the old task themselves
--etagThe dataset’s etag as read; refuse the change if it moved since
--jsonJSON output

Link one episode to a Task, or remove its own link. A link, not an edit: the episode’s task label, revisions and versions stay as they are. Unlinked, the episode reads the Task of the dataset that holds it again. To label and link many episodes, use assign-task; to link a whole upload, link-task.

Terminal window
qualia dataset link-episode <EPISODE_UUID> -d <DATASET_UUID> --task <TASK_UUID>
qualia dataset link-episode accounts/…/datasets/…/episodes/… --unlink --json
OptionDescription
--dataset, -dThe dataset the episode is read through (with a bare id)
--task, -tTask to link: id or accounts/.../tasks/...
--unlinkRemove the episode’s own link
--etagThe episode’s etag as read; refuse the link if it changed since
--jsonJSON output

Pass --task or --unlink, one of them.


What the newest prepare run did to an upload, as the dataset page’s Tasks tab shows it: its state, error and warnings; the tasks discovery found, each with its episode count, health and the Task its episodes are checked against (an existing Task or a new draft); and the dataset’s own task names, with their keys and the decisions on them. A merged dataset or a mixture has no run of its own. Since qualia-cli 0.3.0rc12; 0.3.0rc11 answers “No such command”.

Terminal window
qualia dataset preparation <DATASET_UUID>
qualia dataset preparation <DATASET_UUID> --wait
qualia dataset preparation my-upload-slug --json

Preparation links each found task’s episodes to a Task: one that already existed in the account, when it matches, else a draft it writes. Under the task’s name the table says which where known, then prints the Task’s whole id:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━┳━━━━━━┓
┃ Task, what it is, its id ┃ Episodes ┃ Health ┃ Refs ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━╇━━━━━━┩
│ sorting 2 types of screws │ 6 │ ok │ 3 │
│ existing Task, linked v1 │ │ │ │
│ 3f2a9c1e-5b7d-4e0a-9c11-2a6d8e4f0b31 │ │ │ │
│ │ │ │ │
│ Set the table (2) │ 6 │ ok │ 3 │
│ new draft, not published │ │ │ │
│ 8d41c7b2-90ae-4f36-b5d2-71c3e9a06f58 │ │ │ │
└──────────────────────────────────────┴──────────┴────────┴──────┘
It saysThe Task
existing Task, linked v1A publication predates queueing, or the server explicitly matched a published revision. Checks use the stored link revision, here v1
new draft, not publishedIs a draft preparation wrote for this upload: read it, edit it and publish it
published v1, 9 NovThe publication is known but its origin is uncertain (UNKNOWN in JSON)
Task not found: …Cannot be read any more: deleted, or out of the key’s reach

Publication at or after queueing cannot establish origin. Preparation has no stable completion timestamp: its last-update time can advance after success when calibration finishes. Missing timestamps can also leave origin unknown.

When every episode link uses the current published revision, the state also shows its publication date if known, for example linked v3, published 4 Nov 2025. Older or mixed link revisions never take the current publication’s date.

For each existing Task, a line under the table says how many of the dataset’s episodes are linked to it now, so judged against its text, and prints the command that links them to a Task of your own instead:

6 episode(s) are linked to the existing Task “sorting 2 types of screws” (linked v1: 6 episode(s)): checks use the linked revision's text.
To use a Task of your own instead, keeping their task label:
qualia dataset assign-task -d accounts/<A>/datasets/<D> --task YOUR_TASK --label 'sorting 2 types of screws' --filter 'linked_task = "accounts/<A>/tasks/3f2a9c1e-5b7d-4e0a-9c11-2a6d8e4f0b31" AND task = "sorting 2 types of screws"'

--label is the task label those episodes carry; without it assign-task gives them your Task’s name as their label. Each command filters by its label, even when only one nonempty label exists. Empty labels get a filtered dataset episodes command (follow --cursor for more pages) and dataset link-episode EPISODE --dataset DATASET --task YOUR_TASK for each listed episode; that preserves the empty label.

The report counts the revisions stored in the current links. If a newer one is published it says vN is published. It adds qualia dataset relink-task DATASET only when the dataset links that same Task: the command acts on the dataset’s Task, which may differ from its episodes’ Tasks. Publishing alone does not move the links. When the episodes were linked to another Task since, the line says that none is linked to it now. The one Task of a run that discovery did not split is told apart on its Task: line the same way.

The preparation itself does not say which Task is which, so the command reads each named Task’s revisions once, and for an existing Task the episodes linked to it page by page. Before offering a relink hint, it reads the dataset’s Task link. --json has the same facts in linkedTasks, beside the fields the API sends: task, displayName, taskKey, episodeCount, origin (EXISTING, NEW_DRAFT, MISSING or UNKNOWN), revision, publishTime and, for an existing Task, linkedEpisodeCount, labels, otherLabelCount, linkedRevisions (counts per stored revision) and useOwnTaskCommands. A draft that task discovery kept from an earlier run of the same upload also reads new draft. The SDK retains DRAFT_PUBLISHED and PUBLISHED_DURING_PREPARATION as accepted values for compatibility, but no longer infers them from the mutable last-update time. Since qualia-cli 0.3.0rc14; 0.3.0rc13 prints the id alone, under Task id (draft or match).

A found task with no Task prints the qualia task create line that makes one from its name and picked reference episodes, and the link-task (one task) or link-episode line that links its episodes. A Task id it prints is read with qualia task get and published with qualia task publish.

When task discovery could not sort the takes (fewer than 3, too many it could not read, or an error), a line says why and what the tasks are instead: the recordings’ own task names, or one Task for every episode. Check them before you publish.

Preparation starts by itself after an upload and takes minutes. Automatic checks depend on policy and upload eligibility. Free quality checks default on; paid video (VLM) checks default off and require opt-in. An eligible paid check may run after preparation if the current policy still enables it and admission succeeds.

Read before preparation has ended, the command prints a last line that names --wait. --wait covers preparation only: it reads it until it has ended, then prints it. Each change is a line on stderr, so --json still prints only the record on stdout:

Waiting for preparation to end, at most 3600 s (--timeout changes that).
[0:00] Preparation QUEUED: waiting for a worker.
[0:05] Preparation RUNNING: finding the tasks.
[4:10] Preparation RUNNING: 4 task(s) found; linking their episodes to Tasks.
[4:35] Preparation SUCCEEDED.
The preparation--waitExit code
Has ended SUCCEEDEDPrints it0
Has ended, but task discovery failed or fell backPrints it, with the warning and what the tasks are instead0
Has ended FAILEDPrints it, with its error1
Was SKIPPED (nothing to prepare)Prints it0
Has no runLooks again for 30 seconds (an upload’s run is queued as it lands), then prints that0
Is still QUEUED or RUNNING after --timeoutSays on stderr what the last reading showed; the run goes on, so wait again or read it later1

Task discovery can fail while the run is still RUNNING. --wait says so at once, with the reason, and keeps waiting: the run goes on without it and ends with the recordings’ own task names as tasks, or one Task for every episode. After confirm-task-names, --wait also waits for task discovery to finish running again.

Since qualia-cli 0.3.0rc14; 0.3.0rc13 answers “No such option” to --wait.

OptionDescription
--wait, -wWait until the preparation has ended
--timeoutSeconds --wait waits (default 3600)
--jsonJSON output

Decide which of the dataset’s own task names become tasks, as the Tasks tab’s Use these tasks. Every usable name gets a decision: the ones you pass with --use (or every one with --all) are used, the rest are recorded as a person’s no. Read-only names (outcome words, a code, already a task label) are never sent. The confirmed names start task discovery again, which drafts a Task for each; read it with qualia dataset preparation. Since qualia-cli 0.3.0rc12.

Terminal window
qualia dataset confirm-task-names <DATASET_UUID> --use ts_3f9a0c1b2d4e
qualia dataset confirm-task-names <DATASET_UUID> --all --name "ts_3f9a0c1b2d4e=Fold the towel"
OptionDescription
--use, -uUse this task name as a task, by its key (repeatable)
--allUse every task name that can be used
--nameKEY=NAME: the Task name, at most 32 characters. Default: the earlier one, else the first six words
--runThe run you read; refused when a newer run replaced it
--jsonJSON output

A personal API key decides as the person who made it. A workspace’s service key is refused.


List an episode’s edits, newest first, then the recording as revision 0. Each row names who edited, when, the note, and the fields the edit changed against the revision before it. Rulings (keep, reject) are not edits, and an episode nobody edited lists nothing. The history is the take’s: the same through every dataset that holds it.

Terminal window
qualia dataset history <EPISODE_UUID> -d <DATASET_UUID>
qualia dataset history <EPISODE_UUID> -d <DATASET_UUID> -n 10 --cursor <CURSOR> --json
OptionDescription
--dataset, -dThe dataset the episode is read through (with a bare id)
--limit, -nRevisions per page, 1–500 (default 50)
--cursorNext-page cursor from the previous page
--jsonJSON output: episodeRevisions and nextPageToken

Each revision in --json holds the take’s whole content after that edit.


Sign each camera’s video of an episode, or download them. Each link is short-lived and stops working at its expiry; run the command again for fresh ones. When a camera file holds more than the take, the take’s window in it is shown.

Terminal window
qualia dataset videos <EPISODE_UUID> -d <DATASET_UUID>
qualia dataset videos <EPISODE_UUID> -d <DATASET_UUID> --version stable --download ./videos
OptionDescription
--dataset, -dThe dataset the episode is read through (with a bare id)
--versionThe take as this version holds it: a tag, stable, v0 or a digest prefix
--downloadSave each camera’s video into this directory
--jsonJSON output; with --download, a downloads list of saved files

--download DIR creates DIR if needed and saves the whole file of each camera as DIR/<camera>.mp4, replacing a file of that name. A file is written under a temporary name and renamed once complete, so it is whole or absent. The links go to storage directly; your API key is never sent there. A take in no published version yet has no videos, and --download then exits 1.


List a dataset’s episodes, in recording order unless --order-by. The filter is the one the dataset page’s filter bar writes. Each line is one episode: its id (what the commands below take), index, standing, length after its trim, open issues and task label. Pass - as the dataset to read every dataset of the account; each line then starts with the episode’s full name.

Terminal window
qualia dataset episodes <DATASET_UUID>
qualia dataset episodes <DATASET_UUID> --filter 'state = UNREVIEWED AND open_issue_codes:"jerky"'
qualia dataset episodes <DATASET_UUID> --order-by 'metrics.duration_seconds desc' -n 20 --json
OptionDescription
--filterEpisode filter, e.g. state = KEPT, metrics.duration_seconds < 20, note_count > 0
--order-byindex (default), record_time, metrics.<field> desc, open_issue_count desc, note_count desc
--viewbasic, or analysis to add each episode’s analysis
--versionRead the episodes as this version froze them: a tag, stable or a digest prefix
--limit, -nPage size, 1 to 1000 (default 50)
--cursorCursor from the previous page
--jsonJSON output: episodes, nextPageToken, totalSize

open_issue_count and open_issue_codes cover blocking issues. note_count counts notes: findings that do not block an episode (every recording and graph check finding, and VLM verdicts below their confidence bar). A note has no code to filter by.

--version needs a qualia-sdk whose episode reads take version=; with an older one the command says so and exits 1.


Show one episode as its dataset sees it: standing and who ruled, task, labels, length, trim, subtasks, open issues, and who is editing it in the studio. Pass --dataset/-d with a bare episode id, or the full name accounts/…/datasets/…/episodes/….

Terminal window
qualia dataset episode <EPISODE_UUID> -d <DATASET_UUID>
qualia dataset episode accounts/<ACCOUNT>/datasets/<DATASET_UUID>/episodes/<EPISODE_UUID> --json
OptionDescription
--dataset, -dThe dataset it is read through (with a bare id)
--viewbasic, or analysis to add its analysis curves
--versionRead it as this version froze it
--jsonJSON output: the episode, including its etag

Count what a filter leaves: episodes, recorded time and frames, how many episodes carry each value of a field, and each metric’s quantiles, histogram and outlier fences. This is what the dataset page’s overview and outliers tab show.

Terminal window
qualia dataset summary <DATASET_UUID> --facet state --facet open_issue_codes
qualia dataset summary <DATASET_UUID> --filter 'state != REJECTED' --metric metrics.duration_seconds --bins 10
OptionDescription
--filterEpisode filter
--facetCount per value of this field, e.g. state, task, outcome, open_issue_codes (repeats; default state)
--metricDescribe this metric’s spread, e.g. metrics.duration_seconds (repeats)
--binsHistogram bins per metric (default 30); printed only when given
--jsonJSON output, every count included even when 0; an unfenced metric’s fences are "NaN"

Keep episodes in the dataset’s scope, closing their open issues there. Name the episodes, or pass --filter to keep everything it leaves, as the dataset page’s Keep does. Through a mixture the keep is the mixture’s own, and episodes its uploads do not keep are skipped.

Terminal window
qualia dataset keep <DATASET_UUID> <EPISODE_UUID> <EPISODE_UUID> --reason "checked by hand"
qualia dataset keep <DATASET_UUID> --filter 'state = UNREVIEWED AND open_issue_codes:"jerky"'
OptionDescription
--filterKeep every episode this filter leaves, not names
--reasonWords for the record
--jsonJSON output: every count of the result, and locked

The command prints how many episodes changed, were already kept, and were skipped, and how many open issues it closed. Name episodes or pass a filter, not both. An empty --filter "" is refused, so a blank shell variable never selects every episode.

A take a person has open in the studio is left as it was: their edit lease wins over a key. Those takes are listed on stderr with who holds them, and the command still exits 0. Run it again once they are done. --json puts the same list under locked (the first 100) beside lockedCount.


Reject episodes in the dataset’s scope: they leave training there. Nothing is deleted, and qualia dataset reset withdraws it. Same selection, output and locked takes as keep.

Terminal window
qualia dataset reject <DATASET_UUID> <EPISODE_UUID> --reason "operator in frame"
qualia dataset reject <DATASET_UUID> --filter 'metrics.duration_seconds < 2'
OptionDescription
--filterReject every episode this filter leaves, not names
--reasonWords for the record
--jsonJSON output

Withdraw the keep or reject made in the dataset’s scope, as Ctrl+Z does in the studio. Each episode then stands as derived: unreviewed while a blocking issue is open on it, kept otherwise. Issues the withdrawn keep closed open again. A reset carries no reason.

Terminal window
qualia dataset reset <DATASET_UUID> <EPISODE_UUID>
qualia dataset reset <DATASET_UUID> --filter 'metrics.duration_seconds < 2'
OptionDescription
--filterReset every episode this filter leaves, not names
--jsonJSON output

Put back episodes ingest validation rejected, with the reason the check was wrong. Made in the upload only. Episodes that are not validation rejects are skipped.

Terminal window
qualia dataset restore <DATASET_UUID> <EPISODE_UUID> --reason "the fps metadata was right"
OptionDescription
--reasonWhy the check was wrong (required)
--jsonJSON output

List a dataset’s issues and notes, oldest first. An issue blocks a keep until it is dismissed or its episode is ruled; a note never blocks. Through a mixture the list includes the issues upstream of it, under their own names; filter on dataset = "<the mixture's name>" for its own.

Terminal window
qualia dataset issues list <DATASET_UUID> --filter 'state = OPEN AND blocks = true'
qualia dataset issues list <DATASET_UUID> --episode <EPISODE_UUID> --json
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--filterAIP-160 over episode, code, kind, state, blocks, …
--episode, -eOnly this episode’s issues (id or full name)
--order-bycreate_time (default), create_time desc, code, episode
--limit, -nRows per page, 1-1000 (default 50)
--cursorPagination cursor, as the previous page printed
--jsonJSON output

Count issues per code: open, the distinct episodes with an open one, and dismissed. The dataset page’s issues table. A person’s own words show as (own words).

Terminal window
qualia dataset issues summary <DATASET_UUID>
qualia dataset issues summary <DATASET_UUID> --filter 'blocks = true' --json
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--filterSame language as list
--jsonJSON output

Settle open issues by name or by filter: seen, and the episode wanted as it stands. No ruling moves. Notes and issues already dismissed are left alone. Issues on takes another person is editing stay open; the command prints how many it dismissed, then those takes and who holds them.

Terminal window
qualia dataset issues dismiss <DATASET_UUID> --filter 'code = "idle_tail"' --reason "fine for this task"
qualia dataset issues dismiss <DATASET_UUID> <ISSUE_UUID> <ISSUE_UUID>
Argument / optionDescription
DATASETDataset id, full name or slug: the scope (required)
ISSUESIssue ids or full names; omit to select with —filter
--filterIssues matching this filter
--reason, -rWords for the record
--causeMANUAL (default), or TRIM when a trim cut it away
--jsonJSON output

Name issues or pass --filter, not both. Through a mixture only its own issues are dismissed.

Reopen dismissed issues by name or by filter. A person’s ruling on an episode whose blocking issue reopens is withdrawn: the episode is unreviewed again in this dataset. Takes another person is editing are left as they were and listed.

Terminal window
qualia dataset issues reopen <DATASET_UUID> --filter 'code = "idle_tail" AND state = DISMISSED'
Argument / optionDescription
DATASETDataset id, full name or slug: the scope (required)
ISSUESIssue ids or full names; omit to select with —filter
--filterIssues matching this filter
--jsonJSON output

Raise an issue, or with --note write a note, in your own words, on an episode or the whole dataset. An issue on an episode withdraws a person’s ruling on it in this dataset; deleting the issue puts the ruling back.

Terminal window
qualia dataset issues add <DATASET_UUID> "Gripper slips at the second grasp" -e <EPISODE_UUID> --at 12.5
qualia dataset issues add <DATASET_UUID> "Recorded on the new rig" --note
Argument / optionDescription
DATASETDataset id, full name or slug: the scope (required)
TEXTOne sentence; its first line is the title (required)
--episode, -eThe episode it is about; omit for the whole dataset
--noteWrite a note, which never blocks a keep
--atA moment in the episode, seconds from its start
--bodyMore than the sentence says
--request-idIdempotency key: a retry with it returns the first issue
--jsonJSON output

Withdraw an issue or note you raised. An API key withdraws only what it raised itself. An issue others replied to needs --force, which deletes their replies too.

Terminal window
qualia dataset issues delete <ISSUE_UUID> -d <DATASET_UUID> --yes
Argument / optionDescription
ISSUEIssue id or full name (required)
--dataset, -dThe dataset it is read through; needed with an id
--forceAlso delete replies others wrote under it
--yes, -ySkip the confirmation (required without a terminal)
--jsonJSON output

List the replies under an issue or note, oldest first.

Terminal window
qualia dataset issues replies <ISSUE_UUID> -d <DATASET_UUID>
Argument / optionDescription
ISSUEIssue id or full name (required)
--dataset, -dThe dataset it is read through; needed with an id
--limit, -nRows per page, 1-500 (default 50)
--cursorPagination cursor, as the previous page printed
--jsonJSON output

Reply under an issue or note.

Terminal window
qualia dataset issues reply <ISSUE_UUID> "Trimmed it away" -d <DATASET_UUID>
Argument / optionDescription
ISSUEIssue id or full name (required)
TEXTWhat to say (required)
--dataset, -dThe dataset it is read through; needed with an id
--request-idIdempotency key: a retry with it returns the first reply
--jsonJSON output

Show one dataset: its kind, episode counts, robots, task link, tags, reach settings and etag. DATASET is an id, a full name or a slug (the dashboard address, /datasets/<slug>).

Terminal window
qualia dataset get kitchen-run
qualia dataset get <DATASET_UUID> --json

--json prints the qualia.v1 Dataset.


Change a dataset’s name, description, target robot or reach settings. Only what you pass changes. A merged dataset writes its target robot and reach settings to every upload it holds.

Terminal window
qualia dataset update kitchen-run --name "Kitchen, day 2" --description ""
qualia dataset update kitchen-run --target-robot <ROBOT_UUID>
qualia dataset update kitchen-run --reach-base 0,0.1,0 --reach-heading 0.5
qualia dataset update kitchen-run --reach-reset
OptionDescription
--nameNew display name
--descriptionNew description; '' clears it
--target-robotThe robot it plays on: a robot id or accounts/.../robots/...; '' clears
--reach-basex,y,z metres from the grippers’ midpoint in the robot’s frame, or auto
--reach-headingRotate both hand paths around world up, radians (within ±π)
--reach-resetClear both reach settings
--etagRefuse if the dataset changed since this etag

The reach flags change one value at a time: the command reads the stored settings, changes what you passed and writes them back under the etag it read. Saving them starts nothing; the next reach check uses them. A dataset has no solver of its own: it follows its robot’s, so change the solver on the robot. Saving the reach settings also drops a solver that was stored on the dataset earlier.


Merge uploads into one dataset, or add uploads to a merged one. A merge is final: it cannot be undone. Preview it with --dry-run, which writes nothing.

A new merge takes at least two uploads. For one dataset that grows with every batch, upload the first two batches each on its own (qualia dataset upload DIR NAME --standalone) and merge them once with --name. From then on each batch appends as it is uploaded, with qualia dataset upload DIR NAME --merge-target <MERGED_DATASET>, or without the flag once qualia dataset destination set has saved it. Asked to merge one upload, the command says so and names these steps; --into adds an upload that already exists to a merged dataset.

Terminal window
qualia dataset merge day-1 day-2 --name "Bench" --dry-run
qualia dataset merge day-1 day-2 --name "Bench" --resolve target_robot=accounts/<a>/robots/<r>
qualia dataset merge day-3 --into bench --yes
OptionDescription
--nameName of the new merged dataset (or pass --into)
--intoAdd the uploads to this merged dataset
--descriptionDescription of the new merged dataset
--resolveAnswer a conflict: FIELD=VALUE, or channel_key:CHANNEL=KEY; repeatable
--dry-runList what differs between the uploads
--yes, -ySkip the confirmation (required without a terminal)

A blocking conflict (features, cameras, channel names, fps, where the poses are) settles only by leaving uploads out. The others take one value each: answer them with --resolve; labels and the task default to the uploads’ commonest.


Map native channel names onto canonical keys where the automatic grammar got them wrong, then derive the robot interface again. Renames add to earlier ones.

Terminal window
qualia dataset remap kitchen-run --rename /arm/joint_0=left.joint_0.pos --rename /arm/joint_1=

NATIVE= resets that channel to the key read from its name. A key another channel holds is refused and nothing is written: rename both to swap them.


Ingest an MCAP upload’s recordings again under a new meta/ingest.toml, or read a re-ingest that this command or resolve-poses started.

Terminal window
qualia dataset reingest kitchen-run --config ./ingest.toml --wait
qualia dataset reingest kitchen-run --status <REINGEST_ID> --wait
OptionDescription
--configThe complete meta/ingest.toml; it replaces the published one
--statusRead this re-ingest (its id or full name) instead
--waitWait until it succeeds or fails
--timeoutSeconds --wait waits (default 3600)

The upload’s next generation is published; the old one stays readable and versions cut earlier keep what they pinned. The published config again runs nothing. A config with [episodes] spans re-cuts the upload and is refused while any episode holds a person’s ruling, issue, note or edit. Mixtures, merged datasets and LeRobot uploads are refused.


Resolve a handheld upload’s poses at each hand’s tool centre point, with a gripper’s tracker-to-TCP calibration, or undo that with --root.

Terminal window
qualia dataset resolve-poses kitchen-run --gripper <GRIPPER_UUID> --wait
qualia dataset resolve-poses kitchen-run --root

The calibration is read and sent as it stands now; a gripper without one is refused before anything is sent. The work re-derives the whole upload, which takes as long as checking its video again. Without --wait, follow it with qualia dataset reingest <DATASET> --status <ID> --wait.

The same action as qualia dataset poses --at, which also finds the upload’s capture gripper.


Stop the upload feed reporting a failed or finished upload. The record of why it failed and the uploaded bytes are kept. A live upload is never hidden.

Terminal window
qualia dataset dismiss-upload <REPO_ID>
qualia dataset dismiss-upload <REPO_ID> --through <UPDATED_AT_UNIX>

REPO_ID is repo_id in qualia dataset uploads --json. --through is the updated_at_unix you saw, so a failure recorded after it stays. Organization keys only.


Cancel an upload in progress, from any machine. The uploading process is refused its next step and stops; server-side work stops on its next heartbeat. Nothing is published. Refused once the upload has published. Organization keys only.

Terminal window
qualia dataset cancel-upload <REPO_ID>

List the account’s server-side Hugging Face imports, show one, or cancel one. Organization keys only.

Terminal window
qualia dataset imports
qualia dataset imports <IMPORT_ID> --json
qualia dataset imports <IMPORT_ID> --cancel

Copy a dataset into another workspace, of this organization or another one. The copy is a new dataset whose history starts at the copied version, and it records where it came from. It shares the source’s content, so inside one organization no media bytes are written. It prints what it did:

Terminal window
qualia dataset copy fold-bench --to Bench
# Copied 4.0 GiB, 34 episodes in 0.8 s
# Fold bench: accounts/<account>/datasets/<copy-id>
qualia dataset copy <DATASET_UUID> --to "Partner Lab/Bench" --version v3 --name "Fold bench"
# Copied 4.0 GiB (12 MiB written), 34 episodes in 1.4 s

The dataset is found in the CLI’s workspace (--workspace or QUALIA_WORKSPACE, else the key’s own), by id, full name, slug or display name. --to <workspace> is a workspace of the key’s organization, by name or id. --to <org>/<workspace> names an organization and a workspace, each by name or id. An organization named by name resolves through the places this key can transfer to from the CLI’s workspace, so set --workspace or QUALIA_WORKSPACE for it; without one, name it by ids. A target the rule refuses prints the reason before anything is called.

Inside one organization you need read on the source and Contributor or higher in the target. Into another organization you need Admin or Owner in the source organization, and Contributor or higher in the target. A personal API key acts as its creator. A workspace or service key reaches only its own organization, and a key limited to some workspaces can target only those. A refusal prints the server’s reason and exits 1.

A tagged version that left episodes out is copied with new episode numbers, so the files that mix kept and left-out episodes are rewritten: tables always, and video too when the copy leaves the organization. A video that can’t be cut cleanly is re-encoded, and the result says its pixels changed.

Terminal window
qualia dataset copy fold-bench --to "Partner Lab/Bench" --version v3
# Request id 3f1c…: if this stops, run it again within 24 hours with --request-id 3f1c…
# Copying 26 episodes: 4 of 9 files.
# Copied 1.6 GiB (1.5 GiB rewritten, 3.0 GiB written), 26 episodes in 3 min

Such a copy can run past its call. The command waits and prints how far it got on stderr. Stopping the command stops nothing. The request id prints before the first request: run the same command with --request-id to pick the copy up again, within 24 hours of the first call. --request-id never starts a copy: if the server no longer holds that id, the command says the copy stopped and exits with status 1. Run it without --request-id to start again.

OptionDescription
--toTarget workspace: <workspace> or <org>/<workspace> (required)
--versionVersion tag to copy; default the current version
--nameName of the copy; default the source’s, or <name> (copy) if taken
--no-waitReturn at once with how far a running copy got
--request-idPick up a copy by the id it printed, with the same options, within 24 hours
--jsonJSON output: the dataset, counts, bytes and seconds

Move a dataset to another workspace of this organization. It keeps its id, name, history, versions, checks and flags. You need Admin or Owner in the source workspace and Contributor or higher in the target. A move is refused across organizations (copy instead), while a mixture in the source workspace reads the dataset, and when its name is taken in the target.

Terminal window
qualia dataset move fold-bench --to Bench
# Moved 4.0 GiB, 34 episodes in 0.1 s
OptionDescription
--toTarget workspace of this organization: name or id
--jsonJSON output: the dataset, counts, bytes and seconds

Set a dataset up for the reach solver: the robot it plays on and the placement. A dataset follows its robot’s solver, which is set on the robot. It does what the dashboard’s Setup strip does, so a handheld (UMI) dataset can be set up from a terminal or a pipeline. Only what you pass is written; the rest of the reach settings are kept, and repeating a command writes nothing.

Terminal window
qualia dataset setup <DATASET_UUID> --robot <ROBOT_UUID>
qualia dataset setup <DATASET_UUID> --base 0 0 0 --heading 0
qualia dataset setup <DATASET_UUID> --robot <ROBOT_UUID> --wait
qualia dataset setup <DATASET_UUID> --run-solver

For a handheld upload, picking a robot with --robot also starts the resolve at the TCP, when all of this holds:

  • the dataset is an upload with handheld poses (not a mixture or a merged dataset);
  • every arm of the robot has a TCP (a robot with one bare arm would leave one hand at the gripper root and the other at the TCP);
  • the workspace has exactly one UMI rig of the upload’s kind (the capture gripper) and it has a tracker-to-TCP calibration;
  • the poses are not at the TCP yet, or are there with another calibration.

The resolve uses the capture gripper’s calibration, not the robot’s own gripper’s: the robot only decides when. It re-derives the whole upload, so it takes as long as ingesting it did. Without --wait the command prints the operation and how to follow it (qualia dataset poses --follow). When nothing starts, the command says why and leaves the poses where they are. With several UMI rigs of one kind none is the upload’s: resolve it with qualia dataset poses --at tcp --gripper <RIG>.

The solver is never started by itself. --run-solver presses Run solver once, after waiting for a resolve this command started; follow the run with qualia check reach-status.

OptionDescription
DATASETDataset id, full name or slug (required)
--robot, -rThe robot it plays on: id or accounts/.../robots/...; '' clears it
--base X Y ZWhere the takes’ start sits from the grippers’ midpoint: metres in the robot’s base frame, within 5 m
--headingRotate the paired paths around up, degrees (+/- 180)
--resolve / --no-resolveResolve the poses at the TCP whatever the robot / never (default: by the rules above)
--wait, -wWait for a resolve to finish (minutes to hours)
--run-solverPress Run solver afterwards
--jsonJSON output

Progress of a wait goes to stderr, so --json stays one document on stdout.


Show where a handheld upload’s poses are, or move them. Without --at it prints root (the gripper root’s poses), tcp (each hand’s TCP too) or recorded (a raw recording, which a resolve converts), the rig, the capture gripper, the calibration the poses were resolved with, and whether that gripper’s calibration has changed since.

Terminal window
qualia dataset poses <DATASET_UUID>
qualia dataset poses <DATASET_UUID> --at tcp --wait
qualia dataset poses <DATASET_UUID> --follow <OPERATION_ID>
qualia dataset poses <DATASET_UUID> --at root

--at tcp re-derives the upload with the capture gripper’s calibration (the same command again after the gripper’s calibration changed; with the same one it changes nothing), --at root goes back. A ruling or trim made while it runs fails it: run it again.

OptionDescription
DATASETDataset id, full name or slug (required)
--attcp or root: move the poses (without it: show where they are)
--wait, -wWith --at: wait for the re-derive to finish
--followWait for a resolve you started without --wait: its id or operation name
--gripperWith --at tcp: resolve with this gripper’s calibration instead of the capture gripper’s (required with several UMI rigs)
--jsonJSON output

A mixture is a dataset kept up to date from other datasets. It reads sources: a dataset by name, every upload with one label value, or every upload linked to a Task. A dataset source follows stable (its newest tag, the default), head (what it yields now), or is pinned to one tag. Tag a mixture with qualia dataset version create; delete one with qualia dataset delete.

Make a mixture with its sources, or from a version.

Terminal window
qualia mixture create "Folding, all rigs" \
--source dataset=rig-a-folding \
--source dataset=rig-b-folding@1.2 \
--source label=task:fold@head \
--source task=<TASK_UUID>
# a mixture that reads the dataset as version 1.0 froze it, to curate further
qualia mixture create --from rig-a-folding:1.0
OptionDescription
NAMEDisplay name; with --from optional
--source, -sRepeatable: dataset=<id, name or slug>[@stable|@head|@<tag>], label=<task|robot|operator|method|scene>:<value>[@stable|@head], task=<task>[@stable|@head]
--from<dataset>:<tag>: the dashboard’s Curate from this version. Not with --source
--descriptionFree text
--json{"mixture": {...}, "sources": [...]}

The sources are added together, or none of them. When they are refused, the mixture made stays and the error names it.


List a mixture’s sources in panel order: what each reads, how it follows it, its episodes and a newer tag when a pinned source has one.

Terminal window
qualia mixture sources folding-all-rigs --json

Remove a source from its mixture. Asks first; --yes skips the question.

Terminal window
qualia mixture remove-source accounts/…/datasets/…/sources/… --yes
qualia mixture remove-source <SOURCE_ID> --mixture folding-all-rigs --yes

Keep a dataset source on one tag of what it reads. A pinned source holds its dataset out of no merge, so an upload a mixture reads can then be merged.

Terminal window
qualia mixture pin-source <SOURCE_ID> --mixture folding-all-rigs
qualia mixture pin-source <SOURCE_ID> --mixture folding-all-rigs --tag 1.0

Without --tag it pins the dataset’s newest tag. It refuses when the source follows head and the dataset changed since that tag, since the pin would drop those changes; tag them first, or pass --tag to pin anyway.


The mixtures that read a dataset directly, each with how it follows it.

Terminal window
qualia mixture readers rig-a-folding --json

Register robots, import their models and give them a gripper. A robot owns its gripper: attaching one copies a gripper’s URDF, TCP and jaw into a gripper of the robot’s own, so lining it up or editing its TCP changes that robot only, and the copy is a snapshot (recalibrating the handheld later does not change it). A robot that carries a gripper never swaps it: to try another gripper, copy the robot. Every command takes --json, and a robot is an id, accounts/.../robots/... or its display name, looked up among robots only (show also finds gripper designs).

Terminal window
qualia robot list
qualia robot list --gripper

Robots oldest first (the first is the default target of a dataset). --gripper lists the workspace’s gripper designs, such as the handheld’s, and the copies robots own (each named like its source; the Owner column names the robot) instead. --limit (-n) and --cursor page.

Terminal window
qualia robot show <ROBOT_UUID>

The robot’s type, serial number, whether it has a model, the gripper it carries and where it sits on each arm (guessed until you set the mounts). For a gripper: its TCP, the calibration file and date, and the rig it was copied from.

The same as qualia robot show, for a caller that wants a robot: a gripper is refused, a display name is looked up among robots only, and the etag is printed.

Terminal window
qualia robot get <ROBOT> --json

Terminal window
qualia robot create "Lab arm" --type franka_fr3 --serial SN-1
qualia robot create "Lab arm" --urdf lab_arm.zip

--urdf also imports a file in one step: a .urdf, a .zip of the URDF and its meshes, or a directory. When the file never reaches an import, the robot made for it is deleted again; an import that starts and then fails keeps the robot, and the error names it. With --request-id the robot is kept, and running the command again follows the import instead of uploading again.

OptionDescription
NAMEDisplay name, unique in the account (required)
--type, -tRobot type key (franka_fr3, so101_follower); none for URDF-only
--serialSerial number, free text
--request-idIdempotency key (a UUID4) for a retry
--urdfImport this file in one step
--wait, --no-waitWith --urdf: wait for the conversion (default on)
--timeoutWith --urdf: seconds to wait (default 1800)
Terminal window
qualia robot import <ROBOT_UUID> lab_arm.urdf
qualia robot import <ROBOT_UUID> ./rizon_description # a directory is zipped
qualia robot import <ROBOT_UUID> rizon.zip --no-wait

Replaces the robot’s model with a .urdf, a .zip of the URDF and its meshes, or a directory (hidden VCS folders are left out). The file is uploaded (at most 256 MiB) and the server converts the meshes: a second for a URDF, minutes for a large mesh package. The command waits and prints each state on stderr; --no-wait returns once the import is accepted, and --timeout bounds the wait (seconds, default 1800). A URDF the server cannot use ends failed and the command exits 1 with the server’s reason. A file symlinked within a directory is zipped with its content; a link to a file outside it, a link to a directory or a broken link is refused by name.

Terminal window
qualia robot model <ROBOT_UUID>
qualia robot model <ROBOT_UUID> --solver dls --reach-base 0 0 0
qualia robot model <ROBOT_UUID> --arms-file arms.json

Without an option it prints the model: the import state, solver, each arm’s hand link and TCP, the jaw and the reach base. Options change only what they name.

OptionDescription
--solverpyroki or dls
--arms-fileA JSON list of arms {"side": "left", "hand_link": "...", "tool": {"xyz": [], "rpy": []}, "start_posture": []}; replaces all
--reach-base X Y ZWhere a dataset’s reach check starts when it names no base: metres in the robot’s base frame, within 5 m
--clear-reach-baseRemove it
--etagRefuse if the model changed since this etag
Terminal window
qualia robot copy <ROBOT_UUID>
qualia robot copy <ROBOT_UUID> --name "Lab arm, parallel gripper" --gripper <GRIPPER_UUID>

Makes a new robot from an existing one, to try another gripper or a mount without touching the original. The copy takes the robot’s type, URDF and meshes, arms, solver, jaw, reach base and capsules and a gripper of its own, and shares nothing with it afterwards; its serial number, datasets, reach results and history are not copied. --gripper is keep (the default: a copy of the gripper it carries, with its mounts), none, or a gripper or robot to copy a gripper from instead (mounted at each arm’s hand link: line it up with qualia robot gripper mount). keep and none are matched in any case; any other value must be a robot id (a UUID) or resource name, and is checked before the copy is made. A copy is refused while the model is being imported or the capsules fitted: try again in a minute.

With another gripper this is two calls, the copy and then the attach. If the attach fails, the gripper-less copy stays: the command exits 1 with the attach’s own error and a note naming the copy, which you can give a gripper with qualia robot gripper attach <COPY> --copy-from ... or delete. Run again with the same --request-id to retry; a copy that already carries a gripper is returned as it is.

qualia robot rename and qualia robot delete

Section titled “qualia robot rename and qualia robot delete”
Terminal window
qualia robot rename <ROBOT_UUID> "Lab arm 2"
qualia robot delete <ROBOT_UUID> --force --yes

Deleting a robot deletes the gripper it owns, files included. It is refused while the robot has a model unless --force; datasets that played on it keep a dangling reference. It asks first, or pass --yes. With --json it prints {"robot": ..., "deleted": true, "force": ...}.


Edit a robot’s name, type or serial number. '' clears a type or serial. For the name, qualia robot rename is the same action.

Terminal window
qualia robot update <ROBOT> --name "Lab arm 2" --serial SN-0042
OptionDescription
--nameNew name
--typeRobot type key, e.g. franka_fr3
--serialSerial number
--etagRefuse if changed since this etag
--jsonJSON output

Correct what the import guessed. Only what you pass changes. The command reads the model and saves under its etag, so a change made meanwhile is refused. qualia robot model edits the arms and solver too, and qualia robot gripper jaw the jaw.

The jaw lives in the model of the gripper the robot carries, so --jaw-* and --no-jaw write there: a second write when you also change the arms or solver. --etag covers the robot’s model only. With --json a command that wrote both adds the gripper’s model as carriedGripperModel.

Moving an arm’s hand link starts its posture at rest unless --home sets it, and moves the carried gripper’s mount on that arm along, as the Rigs page does.

Terminal window
qualia robot configure <ROBOT> --arm left=left_link7 --arm right=right_link7
qualia robot configure <ROBOT> --home right=0.2,0,0,0,0,0
qualia robot configure <ROBOT> --solver dls --jaw-joint '{side}_finger_joint1' --jaw-open 0.8 --jaw-closed 0
OptionDescription
--armAn arm’s hand link: left=LINK or right=LINK (repeatable)
--homeAn arm’s home posture, radians, one per turning joint: left=a,b,…
--solverIK solver: pyroki or dls
--jaw-jointThe joint that opens the jaw; {side} is left or right
--jaw-openIts position fully open
--jaw-closedIts position shut
--jaw-homeIts position at home
--no-jawClear the jaw joint
--etagRefuse if the robot’s model changed since this etag
--jsonJSON output

Give a robot a gripper, set the mounts of the one it carries, or take it off, in one command. It does what qualia robot gripper attach --copy-from, mount and detach do.

On a robot that carries none, GRIPPER is copied and the copy is the robot’s own. With no --mount or --baked, it creates a mount at each arm’s hand link with no offset, for each hand the gripper has a TCP in a link for. A gripper with no such TCP gets no mount; set one with qualia robot gripper tcp or calibration, then qualia robot gripper mount.

Naming the gripper the robot carries sets its mounts; without --mount each arm keeps a mount that still hangs on its hand link. Any other gripper is refused: a robot never swaps its gripper, so qualia robot copy it with the other one. --none deletes the robot’s own gripper: it asks first, or pass --yes.

Terminal window
qualia robot carry <ROBOT> <GRIPPER>
qualia robot carry <ROBOT> <ITS_GRIPPER> --mount left=left_link7:left-mount.json --baked right
qualia robot carry <ROBOT> --none --yes
OptionDescription
--noneTake the gripper off, which deletes it
--mountleft=LINK or left=LINK:matrix.json: the gripper’s hand link in LINK’s frame, 16 numbers row-major, metres (default identity). Repeatable
--bakedleft or right: the robot’s URDF has the gripper built in on that arm
--etagRefuse if changed since this etag
--yes, -yWith --none: skip the confirmation
--jsonJSON output

List the datasets played on a robot (their target robot). Takes --limit, --cursor and --json.

Terminal window
qualia robot datasets <ROBOT>

Some actions have two commands. Both work; the last column says what differs.

CommandAlsoWhat differs
qualia robot showqualia robot getget refuses a gripper and prints the etag
qualia robot renamequalia robot update --nameupdate also takes --type, --serial and --etag
qualia robot model with optionsqualia robot configureconfigure sets one arm at a time, the jaw, and moves mounts with an arm
qualia robot gripper jawqualia robot configure --jaw-*configure changes one jaw field at a time
qualia robot gripper attach --copy-from, mount, detachqualia robot carryOne command; see above
qualia robot gripper tcp --filequalia gripper tcpgripper tcp takes a design or a robot, by name among designs
qualia robot gripper calibrationqualia gripper calibrateThe same
qualia robot list --gripperqualia gripper listThe same rows, with the TCP state and rig key
qualia robot show <GRIPPER_ID>qualia gripper getget refuses a robot and looks a name up among designs
qualia robot import <GRIPPER_ID> FILEqualia gripper importgripper import refuses a robot and looks a name up among designs
qualia robot delete <GRIPPER_ID> --forcequalia gripper deletegripper delete refuses a robot, always forces and looks a name up among designs
qualia dataset poses --atqualia dataset resolve-posesposes also finds the capture gripper

The robot’s own gripper. Each command takes the robot that carries it.

Terminal window
qualia robot gripper attach <ROBOT_UUID> --copy-from <GRIPPER_UUID>
qualia robot gripper attach <ROBOT_UUID> --urdf ./parallel_gripper
qualia robot gripper mount <ROBOT_UUID> -m left:left_hand:0,0,0.02 -m right:right_hand:0,0,0.02
qualia robot gripper jaw <ROBOT_UUID> --joint '{side}_finger_joint' --open 0.04 --shut 0
qualia robot gripper tcp <ROBOT_UUID> --hand 'left:gripper_{side}_ee:0,0,0.12' --hand 'right:gripper_{side}_ee:0,0,0.12'
qualia robot gripper calibration <ROBOT_UUID> tcp_calibration.json
qualia robot gripper detach <ROBOT_UUID>

attach gives a robot that carries none a gripper. --copy-from copies a gripper design (the handheld’s, a library gripper) or another robot’s own gripper: its URDF and meshes, TCP and jaw as they are now. --urdf uploads a gripper of the robot’s own (a .urdf, .zip or directory) and imports it; it has no TCP yet, so set one before you mount it. A robot that carries a gripper is refused: qualia robot copy it with the other gripper, or detach first.

mount sets where the gripper sits on each arm and replaces all mounts, one per arm at most. A mount is SIDE:LINK[:X,Y,Z[:ROLL,PITCH,YAW]], the position in metres and the rotation in radians (URDF convention) of the gripper’s hand link in LINK’s frame, where LINK is a link of the robot joined to the arm’s hand link by fixed joints only; SIDE:baked says the robot’s own URDF already has the gripper built in. The gripper’s hand needs a TCP in a link first. attach --copy-from takes --mount too; without it the copy is mounted at each arm’s hand link with no offset, guessed.

jaw names the joint that opens the gripper ({side} stands for left or right) and its position open and shut, checked against the URDF’s limits, and optionally --home, its position at the arm’s home (never inferred; left out, the home the model already has for the same joint is kept). It edits the model the jaw lives in: the carried gripper’s, or the robot’s own when its URDF has the gripper built in or it carries none.

tcp sets the TCP of each hand, SIDE:LINK:X,Y,Z[:ROLL,PITCH,YAW] in a link of the gripper’s URDF ({side} in a link stands for the side); a hand keeps its link once set, and hands not named keep theirs. Or --file a JSON of {"left": {"link": "...", "matrix": [... 16 numbers ...]}, "right": {...}}. calibration replaces the TCPs with a tcp_calibration.json. detach takes the gripper off, which deletes it: its URDF, meshes, TCP and jaw.

On a workspace that has not been migrated yet, a robot may still carry a gripper shared with other robots. jaw, tcp and calibration refuse to edit it by way of the robot (the error names the gripper), because the edit would change every robot that carries it; pass the gripper’s id instead of the robot’s to edit it for all of them. mount is not affected: mounts are the robot’s own.

OptionDescription
--copy-from / --urdfattach: where the gripper comes from (exactly one)
--nameattach --urdf: the gripper’s name
--mount, -mattach --copy-from, mount: a mount, repeat per arm
--joint, --open, --shut, --homejaw: the joint and its positions
--hand, --filetcp: the hands’ TCPs
--filenametcp, calibration: the name recorded with the TCP
--etagRefuse if it changed since this etag
--yes, -ydetach: skip the confirmation

Gripper designs: one gripper’s URDF and the tool centre point (TCP) of each hand on it. A robot takes a copy of its own (qualia robot gripper attach --copy-from, qualia robot carry), so changing a design reaches no robot that already has a copy. A UMI rig is a gripper made from a gripper design (qualia gripper umi-rig, or New rig, UMI in the dashboard) and calibrated (qualia gripper calibrate): handheld uploads are converted at it and resolved at its TCP. A workspace has the UMI rigs someone made and none otherwise; nothing makes one for you. A display name is looked up among designs only, never among the copies robots own (they are named like their source): name a copy by its id, or use qualia robot gripper.

List the workspace’s gripper designs, oldest first, with whether each has a TCP and which are UMI rigs (UMI rig, Rig key). The copies robots own are listed too, named like their source; Owner is the robot that carries one. Same list as qualia robot list --gripper. Takes --limit, --cursor and --json.


Show a gripper: whether it is a UMI rig, its rig key, whether uploads resolve with it, the calibration file and date, and each hand’s link and tracker-to-TCP.

Terminal window
qualia gripper get <GRIPPER>

Register a gripper design from its URDF or .zip, as ”+ gripper” does. The name defaults to the file name; a taken name is refused. When the file never reaches an import, the gripper made for it is deleted again. Takes --name, --wait/--no-wait, --timeout (default 600) and --json.

Terminal window
qualia gripper create parallel_gripper.urdf

Import a new URDF onto a gripper. A TCP set in one of its links goes stale until you set it again. Takes --wait/--no-wait, --timeout and --json.


Delete a gripper design and its model (account owners only). Robots that copied it keep their copy. A robot’s own gripper, named by its id, can be deleted too: it comes off its robot, as qualia robot gripper detach does, unless a live dataset still plays back with it. A UMI rig is an ordinary gripper: a delete removes it for good. Pass --yes when there is no terminal.


Make a UMI rig from a gripper design (a template), as New rig, UMI does in the dashboard: a copy of its URDF and TCP, made once and never re-synced. The rig is the capture rig a handheld recording is converted with, and an upload names it with --rig. A workspace can have several; each is named by its name or id. With a tracker-to-TCP calibration on the design the rig resolves uploads at once; otherwise import its tcp_calibration.json with qualia gripper calibrate. Needs rigs:write on the organization.

Terminal window
qualia gripper umi-rig "KIWI pair" --name "Kitchen UMI"
qualia gripper calibrate "Kitchen UMI" tcp_calibration.json
OptionDescription
--nameThe rig’s name; default <design> UMI
--request-idIdempotency key (a UUID4): a retry returns the first rig
--jsonJSON output

Set where the TCP is on each hand: a 4x4 matrix in one of the gripper’s URDF links. Hands the file leaves out keep theirs. At most 64 KiB. Same action as qualia robot gripper tcp --file, which takes the robot.

Terminal window
qualia gripper tcp <GRIPPER> tcp.json
{
"left": {
"link": "{side}_ee_base_link",
"matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0.1, 0, 0, 0, 1]
},
"right": {
"link": "{side}_ee_base_link",
"matrix": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0.1, 0, 0, 0, 1]
}
}

matrix is 16 numbers, row-major, metres, or a nested 4x4. {side} in a link is replaced by left or right.

OptionDescription
--filenameWhat to call the calibration; default tcp-matrix:<gripper>
--etagRefuse if the TCP changed since this etag
--jsonJSON output

Import a handheld rig’s tcp_calibration.json: each hand’s tracker-to-TCP, and the TCP in the gripper’s end-effector link where the file gives it. It replaces the gripper’s TCP. Takes --filename, --etag and --json. Same action as qualia robot gripper calibration, which takes the robot.

Terminal window
qualia gripper calibrate <GRIPPER> tcp_calibration.json

Manage tasks: what a robot should do and how success shows. A task is a draft of five fields (description, scene, steps, success, failure) and an optional procedure, published as numbered revisions that never change. The Robots and Tasks guide walks through it.

Every qualia task command acts in the API key’s own account; a key acts in no other, so --account and QUALIA_ACCOUNT are not needed. See Authentication. A task is named by its id or its full name, accounts/<ACCOUNT_UUID>/tasks/<TASK_ID>.

List the account’s tasks, newest first, with their latest revision and whether the draft has unpublished changes.

A key restricted to workspaces reads no tasks.

Terminal window
qualia task list
qualia task list -n 100
OptionDescription
--limit, -nPage size, 1–500 (default: 20)
--cursorPagination cursor
--jsonJSON output

Show a task with its draft, or one of its published revisions. --json prints the task as the API returns it; edit that file and pass it to qualia task edit.

Terminal window
qualia task get <TASK_ID>
qualia task get <TASK_ID> -r 2
qualia task get <TASK_ID> --json > task.json
OptionDescription
--revision, -rShow this published revision instead
--jsonJSON output

Create a task. Its draft comes from a file or from --description. Nothing is published. A task needs a name: --name, or displayName in the file.

Terminal window
qualia task create --name "Cube in box" -d "Pick the cube off the table and put it in the box"
qualia task create task.yaml --name "Cube in box"

The file is YAML or JSON. It holds either a task as qualia task get --json prints it (displayName, draft, references), or only the draft fields:

description: Pick the cube off the table and put it in the box
scene: A cube and an open box on a table; the cube's start position varies
steps: |
Grasp the cube
Move it over the box
Release it
success: The cube rests inside the box
failure: The cube falls outside the box
OptionDescription
--nameDisplay name (overrides the file)
--description, -dWhat the robot should do
--successA picked success reference episode (repeatable, 1–3)
--failureA picked failure reference, MODE=<episode> (repeatable)
--recoveryA picked recovery reference, MODE=<episode> (repeatable)
--jsonJSON output

Reference episodes are full names, accounts/.../datasets/.../episodes/.... Pick them in the file or with options, not both.


Save a task’s draft from a file, or single fields from options. A file replaces the whole draft, procedure included. An option replaces only that field, and "" clears it. Published revisions never change.

Terminal window
qualia task edit <TASK_ID> --success "The cube rests inside the box"
qualia task edit <TASK_ID> --steps $'Grasp the cube\nRelease it over the box'
qualia task edit <TASK_ID> task.json
OptionDescription
--nameNew display name
--descriptionWhat the robot should do
--sceneThe scene the task runs in
--stepsThe steps, one per line
--successHow success shows
--failureHow it fails
--etagRefuse if the task changed since this etag
--jsonJSON output

Pass a draft file or field options, not both. A file from qualia task get --json carries the task’s etag, so the save is refused if someone changed the task since.


Publish the draft as the next revision. It needs a description and a success; the other fields are optional. A procedure, when there is one, must have one start, a success outcome, every step reachable and no loops. Publishing also links the unlinked episodes and uploads whose task label is exactly the task’s name.

Terminal window
qualia task publish <TASK_ID> -m "First revision"
OptionDescription
--note, -mWhat changed
--etagPublish only the draft as read at this etag
--jsonJSON output

List a task’s published revisions, newest first: number, note, content hash, who published it and when.

Terminal window
qualia task revisions <TASK_ID>
OptionDescription
--limit, -nPage size, 1–500 (default: 20)
--cursorPagination cursor
--jsonJSON output

Link the unlinked episodes and uploads whose task label names the task. It lists the labels that match exactly beside the near misses (case, spacing, punctuation) in your workspace. Without --label it links the exact matches; with it, the labels you pass. A link already set is never changed, and labels stay as they are.

Terminal window
qualia task link-episodes <TASK_ID> --dry-run
qualia task link-episodes <TASK_ID> -l "cube in box" -l "Cube in box."
OptionDescription
--label, -lA label to link, exactly as listed (repeatable)
--dry-runList the matching labels and link nothing
--jsonJSON output

Delete a task. Only account owners can. A task with a published revision is refused unless you pass --force, which deletes its revisions too. It asks for confirmation; pass --yes when there is no terminal.

Terminal window
qualia task delete <TASK_ID>
qualia task delete <TASK_ID> --force --yes
OptionDescription
--forceDelete its published revisions too
--etagRefuse if changed since this etag
--yes, -ySkip the confirmation

Show the episodes a person picked for a task as references: each one’s id, role (success, failure, recovery), mode and episode.

Terminal window
qualia task references <TASK_ID>
OptionDescription
--limit, -nPage size, 1–500
--cursorPagination cursor
--jsonJSON output

Pick an episode as one of the task’s references, as yourself. A task holds 1 to 3 success references and any number of failure and recovery modes, each with 1 or 2. A failure or recovery names its mode; a success has none. You need read access to the episode’s dataset.

Terminal window
qualia task reference add <TASK_ID> accounts/<ACCOUNT_UUID>/datasets/<DATASET_ID>/episodes/<EPISODE_ID>
qualia task reference add <TASK_ID> <EPISODE_NAME> --role failure --mode slip --start 1.5 --end 4
OptionDescription
--rolesuccess (default), failure or recovery
--modeThe failure or recovery it shows (1–80 characters)
--startRange start, seconds into the episode (with --end)
--endRange end, seconds (with --start)
--jsonJSON output

The episode is its full name, as qualia task references --json or Episode.upload names it.


Remove a reference from the task. The episode is untouched. It asks for confirmation; pass --yes when there is no terminal.

Terminal window
qualia task reference rm <TASK_ID> <REFERENCE_ID> --yes
OptionDescription
--yes, -ySkip the confirmation
--jsonJSON output ({} on success)

Ask a VLM to draft the task’s fields from exactly its picked references. It never publishes. --estimate prices it and starts nothing.

Terminal window
qualia task draft <TASK_ID> --estimate
qualia task draft <TASK_ID> --wait
OptionDescription
--modelqualia-vlm-fast, the default and the only one
--max-costCredit cap
--estimatePrice the whole selection; start nothing
--waitWait for the stored proposal
--jsonJSON output

A draft is made as a person. A personal API key acts as the person who created it, so --estimate and the draft work with it. A service key (owned by a workspace or the organization) is refused (a person or their personal API key must draft a Task).

qualia task draft-get <DRAFT> reads a stored proposal and changes nothing. qualia task draft-apply <DRAFT> saves it as the task’s draft; pass --file <spec.yaml> to save your reviewed version instead. Publish it with qualia task publish after review.


List a task’s VLM drafts: each run’s id, state, model, credits, and error.

Terminal window
qualia task drafts <TASK_ID>
OptionDescription
--limit, -nPage size, 1–500 (default: 20)
--cursorPagination cursor
--jsonJSON output

List the datasets linked to a task, or with --episodes its episodes from every dataset. An episode with no link of its own reads its dataset’s link; the Link column says which. The global --workspace picks the workspace.

Terminal window
qualia task linked <TASK_ID>
qualia task linked <TASK_ID> --episodes -n 100
OptionDescription
--episodesList the linked episodes instead of datasets
--limit, -nPage size, 1–500 (default: 20)
--cursorPagination cursor
--jsonJSON output

Link one episode to a task. A link, not an edit: the episode’s label, revisions and versions stay as they are. --unlink removes the episode’s own link only if it is to this task, and the episode then reads its dataset’s link again. To link a whole upload, use qualia dataset link-task.

Terminal window
qualia task link <TASK_ID> --episode <EPISODE_NAME>
qualia task link <TASK_ID> --episode <EPISODE_ID> --dataset <DATASET_ID>
qualia task link <TASK_ID> --episode <EPISODE_NAME> --unlink
OptionDescription
--episode, -eEpisode id (with --dataset) or full name
--dataset, -dThe dataset an episode id is read through: its id, full name or slug
--unlinkRemove the episode’s own link to this task
--jsonJSON output

Enable fast large-file transfer for a cloned Qualia dataset repo. Run it once inside the clone; it configures the Git filter and adds the tracked patterns to .gitattributes, after which large files sync chunk-by-chunk instead of moving whole.

Terminal window
git clone https://junction-api.qualiastudios.dev/git/<org>/<name>.git
cd <name>
qualia git install
OptionDescription
--pathPath to the cloned dataset repo (default .)

Tracked by default: *.safetensors, *.gguf, *.parquet, *.mp4, *.mcap, *.bag, *.npz, *.npy, *.pt, *.bin, *.tar, *.zip. Patterns already present in .gitattributes are left alone.

The clone’s origin must be the same endpoint the CLI is configured against — the filter refuses a repo whose remote points somewhere other than the configured base URL, so a repo cloned from one environment cannot be pushed to another by changing the URL.

Without an API key the filter cannot move data; qualia git install says so rather than failing later inside a git command.



Running work and what ended recently, as the dashboard’s Running page shows it: ingests, check runs, training jobs and pipeline runs. A status is named under its workspace (accounts/<A>/workspaces/<W>/statuses/<OPERATION>), or under the organization for the organization’s own work.

List running work and work that ended within --recent, most recently changed first. The default is the workspace the CLI acts in (--workspace, else your key’s own).

Terminal window
qualia status list
qualia status list --running
qualia status list --org --recent 7d --json
qualia status list <WORKSPACE_ID> --filter 'service = "pipeline" AND state != FAILED'
Argument / OptionDescription
PARENTWorkspace ID or resource name (default: the workspace the CLI acts in)
--orgThe whole organization: every workspace’s work and its own
--runningOnly work that is queued or running
--recentHow far back ended work goes: 90s, 30m, 24h, 7d (default 24h, max 90 days)
--filterAIP-160 filter on state, kind and service; OR binds tighter than AND
--limit, -nPage size, 1–500 (default 50)
--cursorPagination cursor printed by the previous page

Each line is the operation ID, kind, state, progress, target and last change, and cancellable when qualia status cancel can stop it. A key restricted to workspaces cannot read or cancel statuses.

Ask the service running the work to stop it. Needs jobs:write and a contributor role on the work’s workspace. The status turns CANCELLED a moment after the service accepts; ended work, or a kind that cannot be cancelled, is refused. A running VLM or heuristic check run is cancelled with its pipeline run, which returns the credits held for the episodes it had not read; the run then reads failed, “cancelled by” you. A VLM run that stops answering fails on its own after 30 minutes with no episode finished.

A check run’s id is its operation’s ID, so a check run can also be named as qualia check runs prints it (<check>/<run id>) or by its full name.

Terminal window
qualia status cancel <OPERATION_ID>
qualia status cancel vlm/<RUN_ID>
qualia status cancel accounts/<A>/workspaces/<W>/statuses/<OPERATION_ID> --json

Run a check over a dataset and read what it found. The default check is the VLM check (vlm): a model reads each episode against the Task it is linked to. Confident failures become issues; uncertain readings become notes. An issue holds the episode out of kept until a person keeps or rejects it. A check never keeps, rejects or deletes an episode.

The VLM check costs credits. Run qualia check estimate first: it shows the price and starts nothing. A run never spends more than it reserved.

qualia check also reads the thresholds the episode rules judge against (ranges) and the reach check’s status and results (reach-status, reach-cancel, results).

Every qualia check command acts in the API key’s own account, so --account and QUALIA_ACCOUNT are not needed. See Authentication.

qualia check runs one check by its slug. To run several checks from the catalog in one go, use qualia dataset checks.

Control automatic checks on new uploads without disabling a person’s manual check run. Free quality checks (heuristic) default on; paid video checks (graph) default off. Each half inherits independently: dataset override → workspace default → organization default → platform default. Reading prints on or off and its effective source (dataset, workspace, org or platform); --json also includes the override at the named level.

Terminal window
qualia check policy <DATASET_UUID> # effective dataset policy
qualia check policy <WORKSPACE_UUID> --workspace # workspace default
qualia check policy <ORG_UUID> --org # organization default
qualia check policy-set <ORG_UUID> --org --graph on # opt in to paid video checks
qualia check policy-set <WORKSPACE_UUID> --workspace --heuristic off
qualia check policy-set <DATASET_UUID> --graph inherit # remove dataset override
qualia check policy-set <DATASET_UUID> --heuristic on --graph off --json

--org and --workspace after policy/policy-set choose that policy’s parent, not the CLI’s global --workspace (placed before check). The parent is a UUID or its full accounts/{account}/… name. policy-set requires at least one of --heuristic or --graph, each on, off or inherit; an unmentioned half stays unchanged. Setting inherit removes that half’s explicit override, never the other half’s.

Org and workspace defaults take an Admin or Owner at that level and an API key with opt-in members:write; dataset changes take data:write there. A workspace-bound key cannot set the org default. Switching on starts nothing and does not backfill: Applies to new uploads. Run checks to check existing episodes. qualia check estimate shows the cost of a manual video run before qualia check run. See automatic checks in the SDK for GET/PATCH resource names and inheritance details.

A check run is named with its check: <check>/<run id>, for example vlm/<RUN_ID>, which is how qualia check runs prints each run. Its full name, accounts/<account>/checks/<check>/runs/<run id>, which qualia check run and --json print, works too. Wherever a command takes a check run (qualia check status, qualia check get, --run of qualia dataset checks findings and qualia status cancel), pass either; neither costs a request. A run id alone is refused with what to pass instead: a run is read under its check, so its id names nothing without it.

What a VLM check would read and reserve. Starts and charges nothing. It prints the credits, the episode count (and how many are left out because they are not linked to a published Task), the model it would use and every model offered. It also fails the way the run would, for example when no episode is linked to a published Task. Right after an upload that refusal usually means preparation, which links the episodes, has not ended. While it has not, the error ends with one more line that says so and names the command that waits for it, qualia dataset preparation <DATASET> --wait.

Models are Qualia model tiers: qualia-vlm (the strongest), qualia-vlm-fast and qualia-vlm-lite (images only, the cheapest). Which tiers a check offers is what estimate lists. The VLM check reads each episode’s task graph on qualia-vlm only; naming another tier is refused before anything starts.

Terminal window
qualia check estimate <DATASET_UUID>
qualia check estimate <DATASET_UUID> --max-episodes 50 --max-cost 200
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--episode, -eCheck only this episode (repeatable)
--filterCheck only episodes matching this filter
--model, -mQualia model tier; one of the tiers estimate lists as offered
--max-episodesSample at most N episodes
--max-costCredit ceiling
--jsonJSON output

Pass the same --max-episodes and --max-cost to qualia check run to reserve the same amount.

Start a check over a dataset. Without --check it runs the VLM check. It prints the name of each run it started (one per upload of a merged dataset); --wait waits for each run to end and prints its result.

While another VLM run is on the dataset (the automatic one after preparation, or anyone’s), the VLM run is queued behind it and starts when that ends: Queued <run>: queued behind the automatic VLM check started 10:41 UTC (<run>); it starts when that ends. A queued run reserves credits only when it starts. Two runs queued on one dataset run in the order they were asked for. qualia check estimate answers either way. A VLM run asked for before preparation has linked the episodes is refused as estimate is, with the same line naming the wait. --check reach presses Run solver on a dataset: set the robot first with qualia dataset setup, and follow the run with qualia check reach-status.

Terminal window
qualia check run <DATASET_UUID> --max-episodes 50 --max-cost 200 --wait
qualia check run <DATASET_UUID> --check reach
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--check, -cCheck slug (default vlm); - runs the dataset’s defaults
--episode, -eCheck only this episode (repeatable)
--filterCheck only episodes matching this filter
--model, -mQualia model tier; one of the tiers estimate lists as offered
--max-episodesSample at most N episodes
--max-costCredit ceiling
--wait, -wWait for each run to end; exits 1 if one failed
--jsonJSON output

Show where a run stands: its state, how many issues it raised and resolved, and for the VLM check the model, the episodes planned and the pass, fail and uncertain counts. A run that could not read some episodes still ends SUCCEEDED, with a Result: line such as completed, 4 of 30 episodes not checked and each of those episodes (up to 100) with why. Each also gets an open “Not checked” issue, so it is no longer kept; the next run that reads it closes that issue.

A graph check run that asks questions about what it removed prints them last, under Review health:, as the dashboard’s Review health card shows them: each question, why it asks and a few example episodes, then the spot check. --json has every episode, under vlm.reviewHealth.

Terminal window
qualia check status vlm/<RUN_ID>
qualia check status accounts/<ACCOUNT>/checks/vlm/runs/<RUN_ID>
Argument / optionDescription
RUN<check>/<run id>, as runs prints it, or the full name
--jsonJSON output

List check runs, newest first: one line for each run, under a heading. The first column names the run as <check>/<run id>, which is what qualia check status takes; then its state, how many issues it raised and when it was created (UTC). The lines are plain text, not a boxed table, so the run is never cut to fit a narrow terminal. With no run to list it prints No check runs.

Terminal window
qualia check runs --dataset <DATASET_UUID>
qualia check runs --check vlm --state RUNNING
Run State Raised Created
vlm/550e8400-e29b-41d4-a716-446655440000 SUCCEEDED 13 2026-11-09 21:58
heuristic/660e8400-e29b-41d4-a716-446655440000 SUCCEEDED 0 2026-11-09 21:58
OptionDescription
--check, -cCheck slug; - for all (default -)
--dataset, -dOnly this dataset’s runs
--stateQUEUED, RUNNING, SUCCEEDED or FAILED
--limit, -nRows per page, 1-1000 (default 20)
--cursorPagination cursor, as the previous page printed
--jsonJSON output

The issues and notes a check raised on a dataset, with the model’s reading: the episode, the verdict, the confidence, the moments in seconds it points at, and the text. For what catalog checks wrote per episode, passes included, use qualia dataset checks findings.

Terminal window
qualia check findings <DATASET_UUID>
qualia check findings <DATASET_UUID> --limit 100 --json
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--check, -cCheck slug (default vlm)
--filterMore issue filter, ANDed
--limit, -nRows per page, 1-1000 (default 20)
--cursorPagination cursor, as the previous page printed
--jsonJSON output

One check: its kind, what it checks, the dataset kinds it runs on, and whether this deployment can run it. qualia check get vlm says whether the VLM check is set up here.

Handed a check run instead of a check, it shows where that run stands, as qualia check status does.

Terminal window
qualia check get
qualia check get reach --json
qualia check get vlm/<RUN_ID>
Argument / optionDescription
CHECKvlm, episode-rules, mixture or reach (default vlm); or a check run
--jsonJSON output

The reference ranges the episode rules judge against, in your workspace. Each range is one metric on one robot interface (or one robot): its quantiles and the fences an episode is judged against. An upload’s interface is its robotInterface.interfaceHash (SDK client.datasets.get).

Terminal window
qualia check ranges --interface-hash b3:<HEX>
qualia check ranges --metric duration_seconds --json
OptionDescription
--check, -cCheck slug (default episode-rules)
--interface-hashOnly this robot interface (b3:…)
--robotOnly ranges narrowed to this robot (id or name)
--metricOnly this metric
--filterMore range filter (interface_hash, robot, metric)
--limit, -nRows per page, 1-1000 (default 20)
--cursorPagination cursor, as the previous page printed
--jsonJSON output

Where a dataset’s reach check (Run solver) stands: the robot, the settings a run would use now, the last run with how many takes it solved, and whether it is stale (the robot, model, placement or poses changed since). It reads only; nothing runs by itself. A dataset with no usable robot has no key, and a run would start nothing.

Terminal window
qualia check reach-status <DATASET_UUID>
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--jsonJSON output

Press Run solver with qualia check run <DATASET> --check reach or qualia dataset setup <DATASET> --run-solver.

Stop the Run solver press that is under way. Takes not yet sent to the solver are dropped and takes already solved keep their results: the next run answers them from storage and solves the rest. It prints the status after the cancel.

Terminal window
qualia check reach-cancel <DATASET_UUID>
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--jsonJSON output

What the reach check stored per take: for each handheld or egocentric take on the dataset’s target robot, its state (NOT_RUN where nothing is solved under the current key yet), how many stretches the robot cannot follow and the longest, in seconds.

Terminal window
qualia check results <DATASET_UUID>
qualia check results <DATASET_UUID> --state SUCCEEDED --json
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--check, -cCheck slug (default reach; only reach stores results)
--episode, -eOnly this episode
--statePENDING, SUCCEEDED, FAILED or NOT_RUN
--robotOnly results on this robot (id or name)
--filterMore result filter, e.g. reach.longest_seconds > 1
--limit, -nRows per page, 1-1000 (default 20)
--cursorPagination cursor, as the previous page printed
--jsonJSON output

The checks catalog on one dataset. Catalog checks have an id such as heuristic.jerk or task.graph, a category, and a kind. Heuristic checks are free; VLM checks use a model and cost credits. qualia dataset checks run runs one kind per selection; qualia dataset checks findings reads what they found.

Terminal window
qualia dataset checks list <DATASET_UUID>
qualia dataset checks run <DATASET_UUID> --kind heuristic --category heuristic --validate-only
qualia dataset checks run <DATASET_UUID> --kind heuristic --all --wait
qualia check estimate <DATASET_UUID> --max-cost 200
qualia check run <DATASET_UUID> --max-cost 200 --wait
qualia dataset checks findings <DATASET_UUID> --verdict FAIL

list shows every catalog check by category: its kind, whether it can run on this dataset (or why not) and its last result. The Check column is the id --check takes and is never cut; in a narrow terminal the sentences beside it wrap instead.

run takes the union of --check, --category and --all. Name at least one. --kind pins which checks a category or --all selects; unset, the current catalog selects runnable heuristics (VLM catalog checks are unavailable). It lists skipped checks with reasons, except the VLM check’s internal graph steps. VLM catalog checks cannot run here: use qualia check estimate and qualia check run for the supported VLM check, with --max-cost covering the estimate. An explicit --kind heuristic catalog run needs no credit ceiling.

Option (run)Description
DATASETDataset id, full name or slug (required)
--check, -cA catalog check id, e.g. heuristic.jerk (repeatable)
--categoryEvery runnable check of this category (repeatable)
--allEvery catalog check of the selected kind that can run
--kindPin heuristic or vlm for --all and --category
--episode, -eCheck only this episode (repeatable)
--filterCheck only episodes matching this filter
--model, -mQualia model tier for the VLM checks in the selection
--max-episodesSample at most N episodes
--max-costCredit ceiling; required for VLM runs unless validating
--validate-onlyResolve the selection and its bound; start nothing
--wait, -wWait for the run to end; exits 1 if it failed
--jsonJSON output

What catalog checks found on a dataset, newest first: per check, episode and finding, the verdict (PASS, FAIL or UNKNOWN), what it raised (ISSUE, NOTE or NONE), the Task revision it read (Task rev; draft for an unpublished Task) and a title.

It lists only the findings that stand: per check and episode, those of the newest run that read the episode at its linked Task revision (its own link, else its dataset’s). Publishing a newer revision leaves them standing; after qualia dataset relink-task, an episode has none until a run reads the new revision. --all-revisions lists every run’s findings, superseded ones included.

Terminal window
qualia dataset checks findings <DATASET_UUID>
qualia dataset checks findings <DATASET_UUID> --check task.graph --verdict FAIL
qualia dataset checks findings <DATASET_UUID> --run vlm/<RUN_ID> --json
Argument / optionDescription
DATASETDataset id, full name or slug (required)
--check, -cA catalog check id; - for all (default)
--runOnly run <check>/<run id> (or its full name)
--episode, -eOnly this episode
--verdictPASS, FAIL or UNKNOWN
--all-revisionsEvery run’s findings, not only those that stand
--limit, -nRows per page, 1-1000 (default 20)
--cursorPagination cursor, as the previous page printed
--jsonJSON output