Quick and Dirty method to Deactivate Tulip Operators

Hopefully soon we will get an deactivate endpoint, until then you can use a hybrid approach for deactivation.

Tulip bulk user deactivation (hybrid)

Tulip’s API has no write route for deactivating users, so this uses a hybrid approach: the Users API resolves names to user ids, then a Playwright browser session drives the account-settings UI to do the actual deactivation. When a deactivate endpoint ships, the browser half can be dropped.

Prerequisites

  • Node 18 or newer (the lookup uses the built-in fetch). Check with node -v.
  • A Tulip API token scoped to read the users route (e.g. users:read). The deactivation runs through your logged-in browser session, so the token never needs write access. Save the key and secret somewhere safe.

Setup

1. Make a directory and add the script. Put deactivate-users.ts in a new folder and open a terminal there.

https://github.com/mellerbeck/deactivate-users/blob/main/deactivate-users.ts

2. Install the tooling.

bash

npm install -g tsx
npm install playwright
npx playwright install chromium

3. Capture a logged-in session. This opens a browser; sign in to Tulip, then close the window. It writes your session to tulip-auth.json.

bash

npx playwright codegen --save-storage=tulip-auth.json https://your-instance.tulip.co

Treat tulip-auth.json like a password. It holds live session tokens. Do not commit or share it.

4. Set your credentials in the same terminal window (they do not persist between windows).

macOS / Linux:

bash

export TULIP_BASE=https://your-instance.tulip.co
export TULIP_API_KEY=your-token-key
export TULIP_API_SECRET=your-token-secret

Windows Command Prompt (no quotes, no spaces around =):

cmd

set TULIP_BASE=https://your-instance.tulip.co
set TULIP_API_KEY=your-token-key
set TULIP_API_SECRET=your-token-secret

5. List the users to deactivate. Create names.txt, one full name per line. Names must match what Tulip stores exactly. Blank lines and lines starting with # are ignored.

John Doe
Jane Doe
# this line is a comment

6. Preview first. The dry run resolves every name and shows what would happen, without touching the browser.

bash

npx tsx deactivate-users.ts --file names.txt --dry-run

Each name comes back as OK (one exact match, queued), NOT FOUND, or AMBIGUOUS (more than one match, skipped so nothing wrong gets deactivated). Fix any names you care about before the real run.

7. Run it.

bash

npx tsx deactivate-users.ts --file names.txt

You can also pass names directly instead of a file:

bash

npx tsx deactivate-users.ts "John Doe" "Jane Doe"

How names are resolved

Each name in names.txt is matched against the Tulip Users API by exact full name, then gated by role. The dry run prints one of these outcomes per name:

  • OK — exactly one eligible user. Queued.
  • OK (xN) — several records that share one identical, non-empty badge id. These are treated as duplicate records for the same person, and all of them are queued. Records with differing or missing badges are never auto-grouped this way.
  • NOT FOUND — no active user matches that exact name.
  • SKIPPED — a name matched, but the user does not hold the target role.
  • AMBIGUOUS — two or more eligible users that do not share a single badge id. Skipped entirely, so a genuine name collision between different people never deactivates the wrong account.

Run with --dry-run first and resolve any NOT FOUND / SKIPPED / AMBIGUOUS names before the real pass.

Notes

  • Operators only by default. Only users with the operator role are eligible; anyone else matching a name is reported as SKIPPED, not deactivated. The role is read from the user’s workspaceRoleAssignments in the API response (no extra call), and a user counts as an operator if any of their workspace assignments is operator. To target a different role, set TULIP_ROLE to its userRoleId (for example shop-floor-operator). If your operators use a role variant rather than plain operator, the dry run will surface them as SKIPPED so you can adjust.
  • Names must match exactly. Matching is on the stored full name. A middle name or spelling difference lands the row in NOT FOUND, which the dry run shows before anything happens.
  • Watch the first runs. It runs headed by default. Once you trust it, set HEADLESS=1 to run unattended.
  • Session expiry. When tulip-auth.json goes stale, the script detects the login redirect and tells you to re-run the codegen command from step 3.
  • Already-deactivated users do not appear in the default active-user search, so re-running a name lands it in NOT FOUND rather than erroring. That is expected.
  • One failure does not stop the batch. Per-user errors are logged and the run continues; the final summary reports deactivated / failed / skipped counts.

Leave a comment