Manage

Product

Create, list, update, duplicate, and delete the products of a workspace from the CLI.

Usage

bash
envless product create [name...] [--slug <slug>] [--workspace <w>]
envless product list [--json] [--workspace <w>]
envless product update <slug|id> [new name...] [--slug <new-slug>] [--private|--no-private]
envless product duplicate <slug|id> [<slug|id> ...] [--workspace <w>]
envless product delete <slug|id> [<slug|id> ...] [--yes] [--workspace <w>]

A product groups projects inside a workspace, so envless product works one level above envless link. Every subcommand targets a workspace and never touches variables, which means no passphrase and no workspace key are involved. See Concepts for how products sit between a workspace and a project.

The envless command ships in @goenvless/cli. Install it with npm i -g @goenvless/cli@latest.

Running envless product with no subcommand prints the usage block and exits 0. An unknown subcommand prints ✗ Unknown subcommand: <name>, then the same usage block, and exits 2.

Subcommands

SubcommandAlso acceptedTargets
createadd, newOne new product
listlsEvery product in the workspace
updateeditOne product
duplicatecopyAs many products as you pass
deleterm, remove, delAs many products as you pass

update takes a single product, because a new name and a new slug cannot be shared. duplicate and delete take a list, and every identifier is resolved before either of them writes anything. Repeating an identifier, or passing a slug together with the unique ID of the same product, counts once.

Create

bash
envless product create Billing Platform
envless product create --name 'Billing Platform' --slug billing

The name can be given as bare words after create, in which case they are joined with single spaces, or through --name. Bare words win when you pass both. On a terminal with no name at all, the CLI asks for one.

bash
$ envless product create Billing Platform
┌  envless product create
✔ Added to Acme
│
└  ✓ Billing Platform created. Slug: billing-platform

Names are checked on your machine, after the workspace resolves and before the create request goes out. Under 3 characters fails with A product name needs at least 3 characters. and over 50 with A product name can be 50 characters at most., both printing Aborted. and exiting 2. The interactive prompt applies the same rule inline, so a bad answer is asked again instead of ending the command.

Without --slug, the slug is derived from the name by the API. With it, the value is slugified server side exactly as on update, so --slug 'Billing Platform' lands as billing-platform, and what is left has to be 3 to 50 characters or the call comes back as Product slug should be at least 3 characters long! and exits 1. The success line always prints the slug the API returned, never a local guess, so it is safe to copy into a .envless file or a later --product flag.

A name is required when output is not a TTY. In that case the CLI prints ✗ Missing a name. Run envless product create <name>. and exits 2 rather than waiting on a prompt that nobody can answer.

Server-side refusals are printed verbatim and exit 1. The common ones are a slug clash (A product with slug "billing" already exists in this workspace.), a plan limit (Plan limit reached., which is 1 product on Free, 3 on Dev and 15 on Team; Enterprise has no product limit), and missing permission (The user is not authorized to create products in this workspace.).

List

bash
envless product list

Prints one row per product with the name, the slug, and the unique ID. There is no intro block and no spinner, so the output pipes cleanly into grep, head, or anything else.

bash
$ envless product list
NAME              SLUG              ID
Billing Platform  billing-platform  0192a1b2-c3d4-7000-8000-000000000010
Marketing Site    marketing-site    0192a1b2-c3d4-7000-8000-000000000011

Use --json for scripts. It prints the raw array and nothing else, including an empty [] when the workspace has no products. Every entry is the product record as the API returned it, so it carries far more than the three columns of the table view. Read the fields you need by name, since new ones can be added.

bash
$ envless product list --json
[
  {
    "id": 12,
    "uniqueId": "0192a1b2-c3d4-7000-8000-000000000010",
    "workspaceId": "0192a1b2-c3d4-7000-8000-000000000001",
    "name": "Billing Platform",
    "icon": null,
    "iconUpdatedAt": null,
    "nameUpdatedAt": null,
    "slug": "billing-platform",
    "projectCount": 3,
    "isPrivate": false,
    "deleted": false,
    "deletedAt": null,
    "createdAt": "2026-01-14T09:12:44.310Z",
    "updatedAt": "2026-01-20T16:03:02.881Z",
    "environmentCount": 7,
    "variableCount": 128,
    "lastActivityAt": "2026-01-22T11:47:19.004Z",
    "locked": false
  }
]

In the table view an empty workspace prints › No products in Acme yet. Add one with envless product create. and still exits 0, so a fresh workspace does not fail a script. A failed lookup prints ✗ Could not load products (<error>). and exits 1.

Update

bash
envless product update billing Billing Platform
envless product update billing --slug billing-platform
envless product update billing --name 'Billing Platform' --slug billing-platform
envless product update billing-platform --private

The first word after update is the product, given as its exact slug or unique ID. Everything after it is the new display name, joined with single spaces. --name is read only when no words follow the identifier. --slug sets a new slug, and --private or --no-private changes who can see the product. Pass one of them, or several in the same run.

bash
$ envless product update billing Billing Platform --slug billing-platform
┌  envless product update
✔ Billing (billing)
✔ Billing Platform (billing-platform)
│
└  ✓ Renamed to Billing Platform. The slug is now billing-platform, so update any .envless that still pins billing.

Missing the product reports ✗ Missing the product to update. Run envless product update <slug|id> <new name>. and exits 2. Passing nothing to change reports ✗ Nothing to update. Pass a new name, --slug <new-slug>, --private or --no-private. and also exits 2. The 3 to 50 character rule applies to the new name, checked locally before any request goes out.

Only the fields that actually differ are sent. If the product already carries that name and that slug, the command prints Already set that way, nothing changed., calls nothing, and exits 0. When the slug changes, the outro names both the old and the new slug, because any .envless file or --product flag still pinning the old one stops resolving.

--slug is slugified server side, so --slug 'Billing Platform' lands as billing-platform. What is left has to be 3 to 50 characters, and a shorter one comes back as Product slug should be at least 3 characters long! and exits 1. The check for whether anything differs runs on your machine, on the value you typed, so --slug 'Billing Platform' on a product already at billing-platform is a real write that lands on the same slug.

Visibility

bash
envless product update billing-platform --private
envless product update billing-platform --no-private

--private limits the product to the members you add to it, and --no-private opens it to the whole workspace. Passing both is refused before any request with ✗ Pass --private or --no-private, not both. and exits 2.

bash
$ envless product update billing-platform --private
┌  envless product update
✔ Billing Platform (billing-platform)
✔ Billing Platform (billing-platform)
│
└  ✓ Updated Billing Platform. It is private now, so only the members you add can see it.

Visibility is the one field that is always sent. The CLI never reads the current visibility back, even though envless product list --json carries it as isPrivate, so it has nothing to compare against and cannot skip the write. --private on a product that is already private is a successful write that changes nothing, and it never takes the Already set that way, nothing changed. path.

Server-side refusals exit 1 and are printed as they came back. A slug clash reads A product with slug "billing-platform" already exists in this workspace. A product put out of reach by a plan downgrade reads This product exceeds your plan limit. Upgrade to make changes. Missing permission reads The user is not authorized to update products in this workspace. Changing visibility needs a second permission on top of that one, and so does any update to a product that is already private, so a plain rename can come back as The user is not authorized to manage private products in this workspace.

Duplicate

bash
envless product duplicate billing
envless product duplicate billing marketing-site
envless product copy billing

A copy carries the whole subtree: every project of the product, every environment of those projects, and every variable in those environments. The copy is made server side and the CLI never holds a value, so the ciphertext is moved as it is and there is no reason to run envless passphrase set first.

The new name is the old one with (copy) appended, and the slug is derived from that name, so Billing becomes Billing (copy) at billing-copy. A numeric suffix is added when that slug is taken, giving billing-copy-2, then billing-copy-3. Only the product name and slug change, since the projects inside the copy keep their own names and slugs and so do their environments. A private product stays private, and the members you had added to it are added to the copy.

bash
$ envless product duplicate billing marketing-site
┌  envless product duplicate
✔ Resolved billing, marketing-site
✔ Billing (copy) (billing-copy)
✔ Marketing Site (copy) (marketing-site-copy)
│
└  ✓ 2 duplicated. Every copy carries the projects, environments and variables of the original, still encrypted.

There is no confirmation, because nothing is overwritten or removed. Products are matched by exact slug or unique ID, all of them resolved from one product list before the first copy is made, so an identifier that matches nothing stops the run with Not in Acme: billing-2. Nothing was copied. and exits 1.

Once the set has resolved, copies are made one at a time and a refusal on one product never stops the rest. The outro counts what happened and the exit code is 1 when anything failed.

bash
$ envless product duplicate billing marketing-site
┌  envless product duplicate
✔ Resolved billing, marketing-site
✔ Billing (copy) (billing-copy)
✗ marketing-site failed (Plan limit reached.)
│
└  ! 1 duplicated, 1 failed.

Duplicating a product is checked against two plan limits, since the copy brings its projects with it. Either the product count or the resulting project count can come back as Plan limit reached., since both are counted across the workspace, 1 on Free, 3 on Dev, 15 on Team and unlimited on Enterprise. To copy a single project instead, use envless project duplicate.

Delete

bash
envless product delete billing-platform
envless product delete billing-platform marketing-site --yes

Products are matched by exact slug or unique ID, the two values envless product list prints. A name is not accepted. Every identifier is resolved before the confirmation, so a miss stops the run before anything is deleted, with Product billing-platform not found in Acme. for one identifier and Products not found in Acme: billing-platform, marketing-site for several. Both exit 1.

bash
$ envless product delete billing-platform
┌  envless product delete
✔ Billing Platform (billing-platform)
│
◇  Delete Billing Platform? Its projects, environments and variables go with it.
│  Yes
│
✔ Removed from Acme (with 2 projects, 5 environments, 34 variables)
│
└  ✓ Billing Platform deleted.

There is one confirmation for the whole set and it names every product in it. The default answer is No. Declining it, or interrupting it, prints Aborted. and exits 1, so a refusal never reads as success. Pass --yes or -y to skip the prompt.

bash
$ envless product delete billing-platform marketing-site
┌  envless product delete
✔ Billing Platform (billing-platform), Marketing Site (marketing-site)
│
◇  Delete Billing Platform, Marketing Site? Their projects, environments and variables go with them.
│  Yes
│
✔ Removed Billing Platform (with 2 projects, 5 environments, 34 variables)
✗ Marketing Site was kept (Product could not be deleted.)
│
└  ! 1 deleted, 1 kept.

Deletes run one at a time after the confirmation, and a refusal on one product never stops the rest. The exit code is 1 when anything was kept. When every product was kept, the run ends on Aborted. instead of a count, and also exits 1.

Deleting a product takes its projects, their environments, every variable in them and every access grant on it with it, in one transaction. There is no empty-first rule and no undo, so the confirmation is the only thing standing between the command and the whole tree. The success line reports what went, which is the only receipt you get. Use envless project delete when you only want one project gone.

Without --yes on a non-TTY the command stops before deleting anything with ✗ Refusing to run unattended. Pass --yes to confirm. and exits 1. That is the flag to add in CI.

Deleting a product is also a step-up guarded action. Unless you verified a code for this workspace in the last hour, the CLI prompts for one after the confirmation, verifies it, and reruns the delete once. --yes does not skip the code. Without a TTY the delete fails with This action needs a verification code. Run it from an interactive terminal, or do it from the dashboard., so a job that has to run it unattended needs the workspace's Verification Codes setting turned off, see Workspace.

Flags

FlagDescription
--name <name>Name for a new product on create, or the new name on update. Bare words after the subcommand take precedence over it.
--slug <slug>Slug for a new product on create, or the new slug on update. The API slugifies whatever you pass on either, and derives one from the name when create omits it.
--privateupdate only. Only the members you add can see the product.
--no-privateupdate only. Everyone in the workspace can see the product. Cannot be combined with --private.
--jsonlist only. Prints the products as a JSON array and nothing else.
--yes, -ydelete only. Skips the confirmation prompt. Required when output is not a TTY.
--workspace <w>Workspace slug or unique ID. Falls back to the workspace field of .envless, then to the default set by envless workspace use.

Requirements

You must be signed in with envless login. No passphrase, ENVLESS_KEY, --project, or --env is needed, because these commands read and write product records rather than variables. That holds for duplicate too, since the copy of every variable is made server side and stays encrypted.

The workspace comes from --workspace first, the .envless file second, and the default set by envless workspace use third. With none of them, the command stops on ✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exits 1.

All five subcommands run through the write context, so a ev_sk_ machine key is rejected with ✗ Machine keys are read only. Run envless login to write from this machine. even for envless product list. Machine keys are for reading variables, as in envless sync. See Keys.

The write context also resolves a product when one is named, and these commands never use it. So a stale product entry in .envless, a stale ENVLESS_PRODUCT, or a --product flag pointing at something deleted makes any envless product subcommand fail on that lookup with ✗ Product <name> not found in Acme. Clear the value or re-run envless link. The product update, duplicate and delete work on is always the positional one, never the one --product names.

Exit codes

CodeWhen
0Success, an empty product list, an update that changed nothing, and bare envless product printing its usage
2Unknown subcommand, a name outside 3 to 50 characters, no name on a non-TTY, update with no product or nothing to change, --private together with --no-private, and duplicate or delete with no product
1Not signed in, machine key rejected, unresolved workspace or product, a target that does not resolve, cancelled prompt, declined or unattended confirmation, any API failure, and a duplicate or delete run where at least one target failed

Errors

SituationBehavior
Not signed in✗ Not signed in. Run envless login first. and exit 1
ev_sk_ machine key in use✗ Machine keys are read only. Run envless login to write from this machine. and exit 1
No workspace from flag, file, or stored default✗ Missing --workspace. Pass it inline, run envless link, or pick a default with envless workspace use <slug|id>. and exit 1
Workspace does not resolve✗ Workspace acme not found. or ✗ Could not resolve workspace (<error>). and exit 1
create with no name on a non-TTY✗ Missing a name. Run envless product create <name>. and exit 2
update with no product✗ Missing the product to update. Run envless product update <slug|id> <new name>. and exit 2
update with nothing to change✗ Nothing to update. Pass a new name, --slug <new-slug>, --private or --no-private. and exit 2
--private and --no-private together✗ Pass --private or --no-private, not both. and exit 2
duplicate with no product✗ Missing a product. Run envless product duplicate <slug|id> [<slug|id> ...], or envless product list to see them. and exit 2
duplicate target not foundFails the spinner with Not in Acme: <name>. Nothing was copied., prints Aborted., exits 1
duplicate refused on one product<slug> failed (<error>), the rest still run, the outro reads ! 1 duplicated, 1 failed., exit 1
delete with no slug or ID✗ Missing a product. Run envless product delete <slug>, or envless product list to see them. and exit 2
delete target not foundFails the spinner with Product <name> not found in Acme., or Products not found in Acme: <names> for several, prints Aborted., exits 1
delete declinedPrints Aborted. and exits 1, nothing is deleted
delete refused on one of several<name> was kept (<error>), the rest still run, the outro reads ! 1 deleted, 1 kept., exit 1
delete refused on every productPrints Aborted. and exits 1
delete unattended without --yes✗ Refusing to run unattended. Pass --yes to confirm. and exit 1
API rejects the writePrints the API message as it came back and exits 1