---
key: trickest
name: Trickest
description: Drive Trickest — integrate Claude Code / Cursor / Codex, attach a self-hosted machine, authenticate the trickest CLI, and build/run workflows. Load this first when the user says "integrate with Trickest", "connect my machine", or needs workflows, fleet attach, CLI auth, node edits, or output pulls.
version: 1.2.2
tags: [trickest, cli, platform, workflow, dag, runs, database, sandbox, automation, onboarding, integrate, claude-code, cursor, codex]
---

# Trickest

Trickest is a platform for building and running **workflows**: directed acyclic
graphs (DAGs) of containerized tools and scripts that run on a fleet, emit files,
and turn those files into queryable data. You drive it with the `trickest` CLI
(`trickest <noun> <verb>`) from your shell. In a Trickest sandbox the CLI is baked
at `/usr/local/bin/trickest`; on a local machine the one-line installer puts
`trickest` on your `PATH`. Use the bare `trickest` command — don't hard-code a path.

This skill is the self-contained entry point — enough to build and run a workflow
on its own. For depth on one area, load the focused skill (`workflow-builder`,
`trickest-cli`, `database-tql`, `experiment-loop`, `trickest-sdk`,
`sandbox-workspace`). If it isn't already installed next to this one, fetch any
skill body on demand: `trickest skill ls --output json` returns every skill
(filter by `key`). Use `skill ls`, not `skill get` (that one is admin-only).

## First-time setup (coding agents)

If you are **Claude Code**, **Cursor**, or **Codex** and this host is not yet
wired to Trickest (no `trickest` on `PATH`, or this skill is not installed in
your agent dirs), run the one-command installer **on this machine** — never
inside a Trickest sandbox:

```bash
# Auto-detect Claude Code / Cursor / Codex:
curl -fsSL https://trickest.io/integrate | sh

# Or pin the client. Add --attach when this computer should run workflows
# (Community / self-hosted — enrolls this host as a fleet worker; needs Docker):
curl -fsSL https://trickest.io/integrate | sh -s -- --client claude-code --attach
# …or: --client cursor | codex
```

Headless / CI: export `TRICKEST_TOKEN` from https://trickest.io/settings/api-keys
then re-run (auth is non-interactive).

When it finishes:

1. Confirm with `trickest auth status` (and `trickest fleet ls` if you used `--attach`).
2. **Start a fresh agent session** so the skill loads.
3. Continue with the rest of this document.

If the CLI is already installed and signed in, skip the installer and use the
sections below. If runs fail with "No fleet available", attach this host with
`trickest attach` (still never inside a Trickest sandbox).

## The one law

Every platform operation is `trickest <noun> <verb>`. Never a bare verb, never
`trickest <verb>`. Global flags (`--output`, `--quiet`, `--verbose`,
`--no-color`) go **before** the noun:

```bash
trickest --output json node ls        # right
trickest node ls --output json        # also accepted, but keep the habit above
trickest ls                           # wrong: there is no bare verb
```

Unsure of a verb or flag? Run `trickest <noun> --help` before guessing. Do not
invent syntax.

## Setup and auth

1. **Point at the platform.** The CLI talks to a Trickest Next.js origin that
   serves `/api/*`. Set it once:

   ```bash
   trickest config set preferences.base_url https://trickest.io      # or a local/dev origin like http://localhost:3000
   ```

   `TRICKEST_BASE_URL` overrides the config per command.

2. **Log in.** Prefer the env var over the deprecated `--token` flag (which
   leaks the token into process listings):

   ```bash
   TRICKEST_TOKEN=<token> trickest auth login          # or: trickest auth login --token-file <path>
   trickest auth whoami                                 # confirm: email + vault, live:true
   trickest status                                      # auth + active space + active workflow + base_url
   ```

   `billing status` is separately gated and can return `Invalid token` even when
   every other command works. Never conclude your token is dead from a billing
   failure alone.

3. **Select a workflow.** Workflow-scoped verbs need an active workflow. List
   across every space and filter by name — this is the reliable way to get an
   id. Do not lead with `search`: it is a fuzzy, permission-gated fallback whose
   `--type workflow` filter can return nothing even on an exact-name match.

   ```bash
   trickest workflow ls --all --filter "<name>"         # find its id across ALL spaces
   trickest workflow use <id>                           # activates it (stores the id)
   ```

   **Select by id, not name, when in doubt.** Names are not unique, and a plain
   `trickest workflow ls` lists only the active space — a workflow in another
   space (or with `space_id: null`) will not show until you pass `--all`. That
   is exactly why a bare `workflow ls` or a `search` can miss a workflow that
   plainly exists.

4. **Or create a new one.** Workflow-scoped verbs need an active workflow, and
   creating one needs a space. List spaces, then create into one (it auto-activates):

   ```bash
   trickest space ls
   trickest workflow create "<name>" --space <space-id>   # creates + activates it
   ```

   On a fresh setup, `workflow create` with no `--space` fails with `No space set` —
   pick one from `space ls` first.

## Mental model

- A **workflow** is a DAG you can see on the canvas. Each **node** is one job
  boundary: a library tool, a module (a packaged sub-workflow), a script
  (`python` / `bash` / `golang`).
- Tool/script input values are implemented as auto-created, auto-wired typed
  primitives. Treat them as wiring, not peer nodes; author primitives directly
  only when defining parametric module I/O.
- Nodes pass **files**, not values. A node reads each upstream's output under
  `in/<upstream-node-name>/` and writes its own under `out/`.
- **Script output is JSONL** (one JSON object per line). Each `.jsonl` file
  becomes one queryable table named `<nodeName>_<fileBaseName>`. Generic names
  (`output`, `data`, `result`) collapse to just the node name, so use
  descriptive filenames (`hosts.jsonl`, not `output.jsonl`).
- **Your runtime depends on where you run.** In a **Trickest sandbox** you are uid
  2000 in a Linux container at `/trickest/sandbox` (passwordless sudo); each Bash
  call is a fresh `bash -c`, so `cd`/exports do not persist (use absolute paths or
  chain with `&&`); and platform **projections** (`/workflow/`, `/database/`,
  `/results/`, `/storage/`) are read with the file tools (`Read`/`Glob`/`Grep`), not
  bash `cat`/`ls`. On a **local machine** (e.g. your own Claude Code) none of those
  paths exist — reach the same data through the CLI instead: `trickest node graph` /
  `node info <n>` for the workflow, `trickest database …` for tables, `trickest
  output …` for run outputs, `trickest storage …` for files. The CLI verbs work in
  both environments, so when in doubt, use them.

## CLI grammar and output

- **`--output json|yaml|table`** (default: table on a TTY, json when piped).
  There is no `text` format. Pipe JSON through `jq`. Raw-content verbs
  (`output stdout|stderr|tail`) emit plain text, so do not pipe those to `jq`.
- **stdout vs stderr:** structured data goes to stdout; human confirmations on
  mutating verbs go to stderr, as does the `[trickest:active-workflow] id=…`
  marker. So `WF=$(trickest workflow create … | jq -r .id)` stays clean. Add
  `--quiet` to silence the confirmation.
- **Exit codes:** `0` ok, `1` error, `2` auth, `3` not-found, `4`
  validation/missing-workflow, `5` conflict, `6` forbidden, `7` rate-limited.
- **Destructive verbs need `--force` in a non-TTY** (`workflow rm`, `node rm`,
  `run rm`, `space rm`).

### Noun → verb quick reference

```
workflow  ls [--all] [--filter <q>] | get <id> | create <name> | use <id> | rename | rm | summary | apply
node      add <name> --tool|--script|--module|--primitive | ls | info <name> | set | label | rm | graph | distribute
connect / disconnect <src> <dst> [--flow continue|collect]
run       execute [node] [--no-wait] [--fleet <id>] | ls [--workflow <id>] | get <id> | watch <id> | stop <id> | retry
attach    enroll THIS device as a self-hosted worker (creates a fleet + machine, installs the agent; needs Docker)
output    ls | get <node> | stdout <node> | tail <node> | pull <node> [-o path] | mount
database  ls | schema <t> | preview <t> | detect | live <t> | query "<tql>" --table <t> | rows {add|update|rm}
library   ls [query] [--type tool|module] | info <name>           # fast tool/module discovery
search <query> [--type workflow|skill|tool|all]                   # slower federated fallback
space | fleet | variable | memory | schedule | status | config | skill
```

(`run`, not `runs`. `database`, not `db`. The DAG view is `node graph`, not
`workflow graph`.)

## Building workflows

For any 2+-node edit, prefer the typed `WorkflowApply` tool (atomic, previewable)
when it is available; otherwise use the graph verbs. Tiny edits: `node add` +
`connect`.

```bash
trickest node add discover --tool subfinder --input domain=example.com
trickest node add probe --tool httpx --from discover --input json=true
trickest node add report --script python --from probe --label "Report"
trickest connect probe report                       # auto-detects compatible ports
```

Script node I/O rules (the ones that bite):

- Always `os.makedirs("out", exist_ok=True)` before writing.
- Write **JSONL**, one `json.dumps(row) + "\n"` per record. Never `json.dump`
  a single object or a list — the table detector reads line by line.
- Same keys on every line of a file; missing keys degrade column types to text.
- Datetimes are **ISO 8601** (`YYYY-MM-DDTHH:MM:SSZ`), or time-range queries
  break.
- Read upstream from `in/<upstream-node-name>/`; the directory name is the node
  **name**, not its label. Container paths only — never reference `/workflow/…`
  inside a script.

Set a script body from a file, never by pasting it as a positional argument:

```bash
trickest node set my-node script --file ./my-node.py
```

`--file` is the correct path for any non-trivial body: a shell mangles `${{vars}}`
placeholders (bad substitution) and collapses newlines and indentation when a
body is passed positionally.

Shard one logical node across many inputs with distribution (different from DAG
fan-out, where different tools run in parallel):

```bash
trickest node distribute probe --input file         # batch is the default
```

## Running and iterating

```bash
trickest run execute discover                        # run one node + its upstream chain
trickest run execute                                 # whole workflow (use sparingly while iterating)
```

- **Single-node runs are the iteration unit.** They run the target plus any
  upstream that is not already complete, and **memoize** completed upstream
  nodes, so editing one node re-runs only it and its dependents. Editing a node
  busts its own cache and its dependents'.
- **Confirm completion; do not assume.** `run execute` starts the run; depending
  on build and flags it may return a handle before the run finishes. Read
  `trickest run get <id>` for status, or `trickest run watch <id>` to stream
  until done. With `--no-wait` it returns the run id immediately.
- A returned command **does not mean the backend stopped.** Aborting your wait
  does not stop the run. Use `trickest run stop <id>` to actually halt it.
- **Foreground polling:** sandboxes block foreground `sleep`. Poll in a
  backgrounded loop that exits on any terminal status
  (`COMPLETED|FAILED|STOPPED|ERROR|PARTIAL_SUCCESS`), not just success — silence
  is not the same as success.

```bash
# one-shot "tell me when it finishes", backgrounded
id=<run-id>
for i in $(seq 1 60); do
  st=$(trickest --output json run get $id | jq -r .status)
  case "$st" in COMPLETED|FAILED|STOPPED|ERROR|PARTIAL_SUCCESS) echo "done: $st"; break;; esac
  sleep 12
done
```

Validate stage by stage before a run that has side effects: run the upstream
node, read its stdout/output, confirm it is right, then run the node that writes
out. To tune a workflow against a metric, follow the `experiment-loop` skill
(modify → run → measure → keep or discard).

## Reading outputs and data

```bash
trickest output ls <node>                            # files a node produced
trickest output stdout <node> --run <id>             # raw stdout (logs, stats)
trickest output pull <node> -o ./out.jsonl           # download an output file
trickest database ls                                 # JSONL outputs detected as tables
trickest database live <table>                       # promote + index (blocks ~25s)
trickest database query '<tql>' --table <t> --select a,b --limit 50 --output json
```

- **`output pull` is capped at ~1 MB per file** server-side, and truncates
  mid-line. For a large output, do not rely on `pull`. Either query it through
  the database layer (`detect` → `live` → `query`, which paginates), or have the
  producing script split into smaller files.
- **Counts in TQL:** there is no `count()`. Run the query with `--limit 1` and
  read `total_count` from the response. TQL is a bare filter expression, not SQL
  (`port > 443 AND service ~ "http"`); see `database-tql` for the full syntax.
- **Verify in the real sink.** A workflow can end in a script node that writes
  straight to an external system (a REST API, a database) rather than to a
  Trickest table. When it does, the Trickest run only proves the node ran;
  confirm the data by querying that external system directly, not the Trickest
  output.

## Gotchas that look like your bug but aren't

- **A run FAILED with no node detail — read the run-level `message`.** `run get`
  returns a top-level `message` with the failure reason (e.g.
  `"memoization not possible"`) and a per-node `message`. A run that died during
  orchestration has zero nodes but still carries the reason there. Don't blindly
  retry; read the message first.
- **`"memoization not possible"` after editing an upstream node.** A partial run
  (`run execute <downstream-node>`) tries to reuse cached upstream outputs, but
  editing an upstream node changes the workflow version and invalidates that
  cache, so the partial run fails instead of silently re-running everything. Fix:
  run the changed upstream node(s) first to refresh their cache, then the partial
  run memoizes the rest — or just do a full `trickest run execute`. Editing only
  *downstream* nodes does not hit this (their upstream is still valid).
- **`node graph` / a workflow-scoped verb says "not found" on a workflow you
  just selected.** The workflow may have no version yet (no nodes), or it lives
  in a different space than your active one. Reselect by id
  (`trickest workflow use <uuid>`), then retry. The command is not broken.
- **A run errors "No fleet available" or sits queued forever.** The account has
  no managed fleet and no self-hosted machine online — there is nowhere to run.
  Confirm with `trickest --output json fleet ls` (look for `type: MANAGED` or a
  fleet with `machines.active > 0`). Fix: run `trickest attach` **on the user's
  own machine** to enroll it as a worker. Never run `attach` inside a Trickest
  sandbox — it installs a Docker-based agent on the host, which an ephemeral
  sandbox cannot be; ask the user to run it locally instead.
- **`405 / "Not supported by the Hive backend"`** is by design for
  `schedule ls/get/enable/disable` and `fleet scale/rm`.
  The error names the alternative — use it. Not an auth or arg bug.
- **A subcommand with no verb prints help and exits non-zero.** "It printed help"
  is not success.
- **`node ls` does not return input values or script source.** Use
  `node info <name>` for those. `node info --no-source` omits the body for cheap
  DAG traversal across many nodes.
- **`search` is not how you find a workflow by name.** It is a fuzzy,
  federated fallback: `search --type workflow "<exact name>"` can return `{}`
  even when the workflow exists, and one permission-gated sub-search (e.g.
  `Skills: Forbidden`) can make the whole query error with "All searches
  failed." To resolve a workflow id deterministically, use
  `trickest workflow ls --all --filter "<name>"`, which lists every space.
- **External sinks are write-only and upsert-style.** Most ingest scripts upsert
  by a stable key and never delete. So when a filter or scrape change drops an
  item, its old row **lingers** in the sink as an orphan. Either reconcile in the
  sink (delete rows not in the latest batch) or add a delete-not-in-batch step to
  the writing node. Re-running an upsert does not remove anything.
- **Hive normalizes on save.** Port keys and falsy fields can be rewritten. If a
  later `connect` depends on exact port names, re-read with `node graph` /
  `node info` after a save rather than trusting the pre-save object.

## Verify before you report

A claim is only true if a command you ran printed it. Node listings do not carry
input values or script source; a run returning locally does not mean it
finished; an upsert "ok" does not mean the sink is clean. Read the raw truth
(`run get`, `output stdout`, a query against the real sink) and report that, not
a sanitized version.

## Going deeper

- `sandbox-workspace` — the box, paths, and CLI grammar law (foundation).
- `trickest-cli` — the full verb catalog, flags, and exit codes.
- `workflow-builder` — DAG design, modules, distribution, script I/O in depth.
- `database-tql` — TQL syntax and the table lifecycle (run → detect → live → query).
- `experiment-loop` — autonomously optimize a workflow against a metric.
- `trickest-sdk` — driving the platform from code with `@trickest/sdk`.

Not installed locally? Fetch any of them on demand (every skill body is returned to
any vault member):

```bash
trickest skill ls --output json | jq -r '.[] | select(.key=="workflow-builder") | .content'
```
