site
a site is the top-level scope every operator command nests under (roosts, machines, audit log, quota, deploys, processes — all site-scoped). the site record itself stays read-only — create / update / delete live in the dashboard — but membership is managed here, because it is the part operators script.
verbs:
site list— list sites the caller has access tosite get— print the detail record for one sitesite members— list a site's members and their per-site rolessite add-member— add someone to a site, by uid or emailsite set-role— change a member's role on this sitesite remove-member— remove someone from a sitesite transfer-ownership— hand a site to another user
site set-role and owlette user promote are not the same thing. set-role
changes a role on one site. user promote changes a global role, which
applies to every site that person belongs to. If you want someone to administer
one site, set-role is the command you want.
site list
lists every site the active token has access to, server-resolved from the caller's identity. emits a plain ascii table by default and a { "sites": [...] } envelope under --json (legacy unwrapped shape — kept stable for scripts that pipe into jq).
owlette site list [--json]no verb-specific flags. inherits --json, --profile, and --api-url from the global set.
examples
# human-readable table — id, name, plan, timezone, createdAt
owlette site list# pull just the ids for use in another command
owlette --json site list | jq -r '.sites[].id'# count how many sites you can see
owlette --json site list | jq '.sites | length'backing: GET /api/sites.
site get
prints the full detail record for one site (id, name, plan, timezone, owner, createdAt). under --json the cli emits the raw detail object directly — there is no { "site": {...} } wrapper.
owlette site get <siteId> [--json]no verb-specific flags. the positional <siteId> is required. inherits --json, --profile, and --api-url from the global set.
examples
# human-readable detail block
owlette site get site-1# pull a single field
owlette --json site get site-1 | jq -r '.timezone'# scripted: only proceed if the site is on the pro plan
plan=$(owlette --json site get site-1 | jq -r '.plan')
[ "$plan" = "pro" ] || { echo "skipping — not on pro plan"; exit 0; }backing: GET /api/sites/{siteId}.
site members
lists everyone with access to a site, with the role they hold on that site. emits an ascii table by default (uid, email, role, globalRole) and { "members": [...] } under --json.
owlette site members <siteId> [--json]role is the per-site standing: owner, superadmin, admin or member. globalRole is that person's account-wide role — the two differ on purpose, and seeing both side by side is usually what you came for.
a member's other site memberships are deliberately not returned: listing them would hand a site admin the site ids of every other organisation that person belongs to.
examples
owlette site members site-1# just the admins
owlette --json site members site-1 | jq -r '.members[] | select(.role=="admin") | .email'backing: GET /api/sites/{siteId}/members.
site add-member
adds someone to a site. identify them by exactly one of --uid or --email — supplying both, or neither, fails locally without spending a request.
owlette site add-member <siteId> (--uid <uid> | --email <email>) [--role member|admin] [--idempotency-key <key>]--role defaults to member. requesting admin only takes effect if that person's global role is already admin or superadmin; otherwise the membership is still added and the cli prints a WARNING ... NOT honored line. adding someone to a site never promotes them.
re-adding an existing member is a no-op and returns success. the site's owner cannot be re-added at a lesser role — that returns 409 target_is_owner.
examples
# the usual case: you know a colleague's address, not their uid
owlette site add-member site-1 --email alice@example.com# add as a site admin (only honored if their global role already allows it)
owlette site add-member site-1 --uid u_123 --role adminbacking: POST /api/sites/{siteId}/members.
site set-role
changes a member's role on this site only. their global role is untouched.
owlette site set-role <siteId> <uid> <admin|member> [--idempotency-key <key>]owner is not a settable role. ownership moves through site transfer-ownership, and the cli rejects set-role ... owner locally with a pointer to it. the site owner also cannot be demoted through this command — that returns 409 cannot_change_owner_role.
examples
owlette site set-role site-1 u_123 admin# demote back to a read-only operator on this site
owlette site set-role site-1 u_123 memberbacking: PATCH /api/sites/{siteId}/members/{uid}.
site remove-member
removes someone from a site. idempotent: removing a non-member succeeds and reports was not a member.
owlette site remove-member <siteId> <uid> [--talon-successor <uid>]talons that person authored survive the removal — they belong to the site, not the author — but a talon with a hoot output re-resolves its author's site access on every run, so it starts failing silently once the author is gone. the cli prints a WARNING ... talon(s) ... left in place line when that happens. pass --talon-successor <uid> to reassign them first; reassignment is never implicit.
the site owner cannot be removed (409 cannot_remove_owner) — transfer ownership first.
examples
owlette site remove-member site-1 u_123# hand their automations to someone else on the way out
owlette site remove-member site-1 u_123 --talon-successor u_456backing: DELETE /api/sites/{siteId}/members/{uid}.
site transfer-ownership
hands a site to another user, atomically.
owlette site transfer-ownership <siteId> <successorUid> [--idempotency-key <key>]only the current owner or a superadmin may transfer; a site admin is refused with 403 not_owner — administering a site and owning it are different things. the successor does not need to be a member already; they are made one by the same transaction. the outgoing owner is demoted to admin and keeps their membership, because handing a site over is not the same as leaving it.
transferring to the current owner returns 409 successor_already_owner rather than silently succeeding.
examples
owlette site transfer-ownership site-1 u_456# leaving the company: transfer, then remove
owlette site transfer-ownership site-1 u_456
owlette site remove-member site-1 u_departing --talon-successor u_456backing: POST /api/sites/{siteId}/transfer-ownership.
exit codes
0— success (table printed or detail emitted)1— network failure or non-2xx api response (e.g.404if<siteId>doesn't exist or you lack access)2— no token configured for the active profile (runowlette auth loginfirst)
notes
- scope: user — lists sites the caller's token has access to; no
--siteflag because the noun is the site - tier:
[ready]. the site record is read-only here —create,updateanddeletestay in the dashboard — but membership is fully managed from the cli - roles: per-site (
owner/admin/member) are distinct from the global account role.site set-rolechanges the former;owlette user promotechanges the latter and affects every site that person belongs to - json envelope:
listemits{ "sites": [...] }(legacy unwrapped shape);getemits the raw detail object. new commands wrap in{ ok, data }— these two predate that convention and stay stable for compatibility - related: overview (auth + config precedence), roost, quota, and every other operator noun (all consume
--site <siteId>against the ids returned here)
roost
a roost is a content-addressed bundle of files (a touchdesigner project, a media payload, a build artifact — anything) that the cli pushes once and the agent then pulls atomically onto target machines. every roost is site-scoped: every verb on this page requires --site . tier: [ready] for all six verbs.
swoop
swoop opens the remote-desktop viewer for one machine in your browser. it reads the machine record, prints the viewer url and opens it — nothing else. the session itself, and the second factor control asks for, happen in the browser. tier: [ready].