> For the complete documentation index, see [llms.txt](https://developer.celigo.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.celigo.com/cli/local-tree/promote.md).

# Promote to another account

`celigo promote --to <profile>` writes a [local tree](/cli/local-tree.md) into **another** account or environment: it creates what the target lacks and updates what it holds, with every reference remapped to the target's ids. Sandbox to production inside one account and account to account are the same mechanism — each environment's API token is its own principal, and the two never see each other's ids.

The source is the tree, bound to the account it was pulled from. Promote never rebinds it, never writes its files or its manifest, and never takes `--profile`; the only account it touches is the one `--to` names. State per target lives in a committed `.celigo/targets/<name>.json`, so the second promote is an update rather than a second copy, and a teammate or a CI job at the same commit promotes to the same target ids.

**Required:** full mode on the **target** profile ([The target's mode](#the-targets-mode)). Both sides are announced on stderr before the first write: `celigo: promote tree of profile 'prod' (api.integrator.io, account user 624c…) → profile 'sandbox' → api.integrator.io`.

**Signature**

```bash
celigo promote --to <profile|target> [refs...] [flags]
```

**Arguments**

| Argument    | Type                             | Required | Description                                           |
| ----------- | -------------------------------- | -------- | ----------------------------------------------------- |
| `[refs...]` | file path, `type:id`, or bare id | No       | Narrow the promote to these resources, as for `push`. |

**Flags**

| Flag                            | Type               | Default | Description                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------- | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--to <profile\|target>`        | string             | —       | **Required.** The target: a profile name, or the name of an existing target file under `.celigo/targets/`.                                                                                                                                                                                                                                                              |
| `--dir <path>`                  | string             | `.`     | Tree root directory.                                                                                                                                                                                                                                                                                                                                                    |
| `--dry-run`                     | boolean            | `false` | Compute and print the plan without writing anything. Exits `1` when any item is refused.                                                                                                                                                                                                                                                                                |
| `--integration <id\|path>`      | string             | —       | Narrow to one integration: its document, everything carrying its `_integrationId`, and the closure of what those reference in the tree.                                                                                                                                                                                                                                 |
| `--map <type/source=target-id>` | string, repeatable | —       | Pin a per-environment resource — a connection, iClient, MCP OAuth provider, stack, or on-premise agent — to a target id, for example `connection/connections/Shopify/connection.json=<target id>` or `mcpoauthprovider/mcp-oauth-providers/Okta/mcp-oauth-provider.json=<target id>` (a source path or a source id). Persisted in the target file, so it is given once. |
| `--create-connections`          | boolean            | `false` | Create a credential-less shell in the target for a connection, iClient, MCP OAuth provider, stack, or on-premise agent that has no namesake and no pin.                                                                                                                                                                                                                 |
| `--adopt-by-name`               | boolean            | `false` | Map a same-named target resource in the same place to the source file and update it, instead of refusing to create a second one. A topic carrying an `externalId` matches the target topic with the same `externalId` first, whatever its name.                                                                                                                         |
| `--prune`                       | boolean            | `false` | Delete the target copies of resources removed from this tree. Asks for confirmation.                                                                                                                                                                                                                                                                                    |
| `--force-remote`                | boolean            | `false` | Overwrite (or delete) target copies that changed since the last promote.                                                                                                                                                                                                                                                                                                |
| `--allow-account-mismatch`      | boolean            | `false` | Promote into a different account than the target file is bound to (host or token owner), rebinding it.                                                                                                                                                                                                                                                                  |
| `--allow-duplicate-names`       | boolean            | `false` | Skip the namesake listings in the target: the plan-time pre-flight and the re-check before each type's first `POST`.                                                                                                                                                                                                                                                    |
| `-y, --yes`                     | boolean            | `false` | Skip the delete confirmation prompt (`--prune`).                                                                                                                                                                                                                                                                                                                        |
| `--concurrency <n>`             | integer            | `8`     | Requests in flight at once (1–16). `CELIGO_CONCURRENCY` sets the default. Writes still run in dependency order, deletes dependents-first.                                                                                                                                                                                                                               |
| `--force-lock`                  | boolean            | `false` | Take the tree lock even if another pull, push, or promote appears to hold it.                                                                                                                                                                                                                                                                                           |
| `--allow-lint-errors`           | boolean            | `false` | Send a document the plan-time logic lint refuses. The rules still run on every planned document and every finding is printed: an error becomes a `note: lint error: … (error allowed by --allow-lint-errors)` line instead of refusing the item. Schema findings never gate a promote. See [The plan](#the-plan). Added in celigo-cli 2026.10.1.                        |

**Example** — the first promote of an integration into a sandbox environment, then the repeat:

```bash
# From the production profile: the tree's source
celigo pull --dir ./acme --integration 5f83a9b2c7d3e8f1a2b3c4d5

celigo promote --to sandbox --dir ./acme --dry-run
#   Plan for 'sandbox': 5 to create, 1 refused
#     create  script:new       scripts/Order hooks/script.json
#     create  export:new       exports/Get Orders/export.json
#     …
#     refused: connections/Shopify/connection.json — connection 'Shopify' has no namesake in the
#       target — connections are never promoted with their credentials; pin the target's
#       connection with --map connection/connections/Shopify/connection.json=<target id>, or pass
#       --create-connections to create a credential-less shell named 'Shopify' there …

celigo promote --to sandbox --dir ./acme \
  --map connection/connections/Shopify/connection.json=6a1b2c3d4e5f6a7b8c9d0e1f
#   map:    connection 'Shopify' → 6a1b… (pinned with --map)
#   created  integration:65b2…  integrations/Acme/integration.json
#   …

git add .celigo/targets/sandbox.json && git commit -m "Promote Acme to sandbox"

# Nothing changed since: no per-resource request, "5 unchanged"
celigo promote --to sandbox --dir ./acme
```

## The target file

`.celigo/targets/<name>.json` is committed beside the manifest (pull's `.celigo/.gitignore` already leaves `targets/` tracked) and validated on load with the manifest's rigor. It records:

* every source `type:id` → the target copy's id, with the target's `lastModified` lock token, the hash of the copy that write left there (`heldHash`), and the hash of the body last sent;
* how each connection, iClient, MCP OAuth provider, stack, and on-premise agent resolved (`name`, `pin`, `adopted`, `shell`);
* the flow-group id map;
* the create-intent markers of an interrupted run;
* the target's identity: host plus the token owner's user id.

It holds ids, hashes, and timestamps — never an API token.

`--to` resolves to an existing target file first, then to a profile; the first promote creates the file under the profile's name. The recorded profile name is a label: a machine with no profile named `production` still promotes to the committed `production` target, provided it has the target's token (below). A merely renamed profile needs no override — edit the `profile` label in the file.

## The target's token

The target is reached with the `--to` profile's stored credential — its API token or, since celigo-cli 2026.10.1, an OAuth sign-in made with [`celigo login --profile <name>`](/cli/commands/login.md) — or, on a CI runner with no such profile, with `CELIGO_TARGET_API_TOKEN` and `CELIGO_TARGET_BASE_URL` (default `https://api.integrator.io`). A note says when the variable stands in, and the variable wins over a stored credential when both exist. The variable stands in for a profile only behind a committed target file, whose recorded host and account user the token is checked against: on a machine with neither the file nor a profile of that name, the promote is refused and says to run the first promote where the target has a profile, then commit `.celigo/targets/<name>.json`. A signed-in target's session goes only to the host it was issued for, so a `CELIGO_TARGET_BASE_URL` naming another host is refused before any request.

`CELIGO_API_TOKEN` and `CELIGO_BASE_URL` belong to the **source** session and are never applied to the target. Since celigo-cli 2026.10.1 promote refuses `--token` and `--base-url` outright, as it refuses `--profile`, instead of ignoring them: store the target's token on its profile or export `CELIGO_TARGET_API_TOKEN`, and set `base_url` on the target profile or export `CELIGO_TARGET_BASE_URL`. A target with neither a stored credential nor the variable is refused, naming the ways to give it one — a stored token, an OAuth sign-in, or the variable — instead of promoting the tree back into the source account under the target profile's name. `--to` naming the tree's own account is refused too: use `celigo push`.

## The target's mode

Promote needs full mode on the target, and only there. The target's mode is the target profile's stored `mode`, and since celigo-cli 2026.10.1 `CELIGO_MODE` can only tighten it: `CELIGO_MODE=read` or `operate` in the environment refuses the promote before any request, and `CELIGO_MODE=full` cannot lift a target profile stored in `read` or `operate`. A target with no profile on the machine (`CELIGO_TARGET_API_TOKEN`) is `full` unless the variable tightens it. The refusal names what decided — the profile's stored mode, or the variable:

```
target profile 'production' is in read mode, and promote writes structural documents into the target — it needs full mode there. A person raises that profile's mode at a terminal with 'celigo config set mode full --profile production'; an agent must not raise it to get past this refusal (diff --target reads a target in any mode).
target profile 'production' is in read mode (from CELIGO_MODE, which tightens the target's mode and never loosens it), and promote writes structural documents into the target — it needs full mode there. Whoever set CELIGO_MODE=read in this environment unsets it for the promote; an agent must not change it to get past this refusal (diff --target reads a target in any mode).
```

The source profile is never used, so its mode does not gate a promote: a production source profile kept in `read` mode promotes into a full-mode sandbox. Earlier releases refused that promote and suggested switching the source profile to full. `diff --target` only reads, so it runs in any mode of the target profile. See [Permission modes](/cli/getting-started/profiles.md#permission-modes).

**Under `profile_pin_policy = strict`**, a target profile that is not the active profile needs a person at a terminal (since celigo-cli 2026.10.1). After both sides are announced and before any request, promote asks for the profile's name to be typed back; `-y` does not answer this prompt:

```
Promote into profile 'sandbox' (api.integrator.io)? It is not the active profile ('prod'), and profile_pin_policy is strict on this machine. Type 'sandbox' to confirm:
```

Anything else stops the run with `Not confirmed; nothing was promoted to 'sandbox'.` Without a terminal the promote is refused:

```
The promote target, profile 'sandbox', is not the active profile ('prod'), and this machine refuses pinned profiles that differ from it (profile_pin_policy = strict): a promote into it needs a person at a terminal to type the profile's name back — run this command in an interactive shell. If 'sandbox' is the account you mean, a person switches to it with 'celigo profile use sandbox'; an agent must not switch profiles or change profile_pin_policy to get past this refusal.
```

A target with no profile on the machine — `CELIGO_TARGET_API_TOKEN` behind a committed target file, the CI shape — asks nothing, and `diff --target` never asks. See [Refusing a pinned profile](/cli/getting-started/profiles.md#refusing-a-pinned-profile).

## The target's identity

Promote verifies the target's account before anything is read — one `GET /v1/tokenInfo`, never retried — and is stricter about it than push. A target file bound to another account is refused unless `--allow-account-mismatch` rebinds it, which starts its map, bindings, and group ids over (they name the old account's resources), so the next promote creates everything again there and says so.

When the probe fails (a known platform issue makes `GET /v1/tokenInfo` return `500 Internal Server Error` for some production tokens) — or, since celigo-cli 2026.10.1, answers without naming the token's account (no `ownerUser`, the answer an OAuth sign-in gets when the platform cannot confirm its account grant) — `push` degrades to the host match with a warning. Promote does the same only for a target not yet bound to an account user — the first promote. A **bound** target is refused, because a promote writes into another environment under a second token and the host alone cannot tell that token from a colleague's on the same host. Retry once the probe answers, or pass `--allow-account-mismatch` to accept the host match for that run; the binding is kept as recorded.

## The plan

Plan first, in push's vocabulary. `create`, `update`, and `delete` are sent. `unchanged` (the body last sent has not changed — no request) and `up to date` (the target already holds exactly this body — its token is adopted, no request) cost nothing. A `map:` line per per-environment resource says how it resolved: `map: connection 'Shopify' → 64f… (matched by name | pinned with --map | shell to be created)`. `refused: <path> — <reason>` and `blocked: <path> — <pointer> names <path>, whose promote is refused` fail the run — exit `1`, from `--dry-run` too. `withheld:` (another integration's resources; an Integration App's documents — see [An Integration App installed in both environments](#an-integration-app-installed-in-both-environments)) is not sent and not a failure. `note:` lines on stderr carry orphaned target copies, dropped group ids, literal-id splices in scripts, registrations, lint warnings, and the [Event Streams](#event-streams-topics) notes.

Every plan-time guard [push](/cli/local-tree/push.md#what-push-refuses) applies: a document whose `_id` is not the tracked resource's, masked credentials, the `responseMapping` object wrapper or `fields: null`, an unresolved reference — a placeholder, an `@bindings/` key, or an id naming nothing in the tree, and a tracked resource left out by refs or `--integration`, which the refusal says. Since celigo-cli 2026.10.1 the guards also refuse a create whose document carries the server's mask anywhere, on every type; a value that is not a string in a field the platform encrypts, in a promoted document and in a `--create-connections` shell alike; and a draft topic. A mask is four or more asterisks and nothing else, so a lone `*` — a wildcard header — is a value. A refused create blocks its dependents; a refused update does not, since its target copy is there to point at. Promote works from the manifest: a resource file the tree does not track (a draft never pushed to the source) is not promoted, and a note says to push it first.

Since celigo-cli 2026.10.1 promote reads the tree as `celigo status` does. A folder moved locally is promoted from where it sits. A resource `.celigoignore` keeps out of reach, or one parked in an ignored folder, is left alone — named as a ref, it is `withheld:` — and never pruned. A resource an interrupted pull or push left unfinished is refused with what finishes it (`celigo pull`, or `celigo push` for an interrupted create) and never pruned. Earlier releases refused all of these as missing on disk. See [Status and diff](/cli/local-tree/status-and-diff.md#celigo-status).

**The logic lint runs at plan time** (since celigo-cli 2026.10.1). Every planned create and update is checked by the six logic rules `celigo lint` runs — `hook-function`, `script-syntax`, `lookup-reference`, `router-reference`, `handlebars-syntax`, `topic-binding` — under push's severity rule: a finding in, or reading, a file that is new or changed since the pull is an error and refuses the item (`… (lint <rule>; pass --allow-lint-errors to send it anyway)`), and one where every file involved is unchanged is a `note: lint warning: …` line, so promoting a pristine pull is never refused for what the source account already holds. A planned script is held to every tracked document whose handler runs it. `--allow-lint-errors` keeps the rules running and prints an error as `note: lint error: … (error allowed by --allow-lint-errors)` instead of refusing the item. Schema findings never gate a promote. [The logic lint gate](/cli/local-tree/push.md#the-logic-lint-gate) and [Logic rules](/cli/local-tree/lint-and-resolve.md#logic-rules) describe the rules.

**References are rewritten through the map**: declared references, ids embedded in URIs, and — disclosed one splice at a time — literal ids in script source and `settings` whose target copy is known. Writes run in dependency order, then deletes dependents-first; a reference cycle is created without the deferred half and repaired by a disclosed fixup `PUT`. The legacy `sandbox` flag never travels. Flow groups travel by name: an integration's `flowGroupings` lose their ids on create and keep the target's on update, and a flow's `_flowGroupingId` is remapped, or dropped with a note until the group exists there. An integration's registered connections, lookup caches, and topics are registered after it lands. Since celigo-cli 2026.10.1 the connection or lookup cache an integration alias names is registered there even when the source's registration lists omit it, as push does, a promote retried after a partial registration completes it, and a child integration's parent (`_parentId`) is remapped like any other reference.

**Repeat semantics.** A source whose last-sent body is unchanged sends nothing: a no-op promote of a whole tree is the identity probe plus one listing per per-environment type. A changed source costs two `GET`s — the lock, judged when the plan is made and again right before the `PUT` (since celigo-cli 2026.10.1; `--force-remote` skips the re-check) — and one `PUT`. A target copy someone edited in the target's UI since the last promote is `refused` until you look: `celigo diff --target <name> <path>` to inspect, then `--force-remote` to overwrite, or pull the target and carry the change back into the source. A flow that only ran in the target, or whose debug window was toggled there, is not an edit: its `lastModified` moved, but the copy still hashes to what the last promote left (`heldHash`). Since celigo-cli 2026.10.1 the target file records that hash for every copy a promote writes, so such a copy is no longer refused; an entry an older CLI wrote keeps the token-only lock until its next write. A target copy that no longer exists is created again with a note, and the documents that reference it are re-pointed in the same run; because an unchanged source costs no request, a copy deleted in the target is noticed once its source changes, while [`celigo diff --target`](/cli/local-tree/status-and-diff.md#diff-against-a-promote-target) shows it as `deleted in target` at any time.

## Connections and the other per-environment resources

Connections, iClients, MCP OAuth providers, stacks, and on-premise agents are never promoted with credentials: the tree holds `******` placeholders, and a copy without its secret is useless anyway. MCP OAuth providers joined this class in celigo-cli 2026.10.1, like iClients — the API masks a provider's `clientSecret`, and each environment trusts its own issuer — so an MCP server's provider resolves in the target instead of being sent. Each one the promoted documents reference resolves in the target in this order:

1. An explicit **pin** — `--map connection/<source path or id>=<target id>` (`--map mcpoauthprovider/…` for a provider) — persisted in the target file.
2. The **persisted binding** from an earlier promote, if the target still has it. An MCP OAuth provider an older promote created as a document counts too: it stays the binding (`adopted`), and `--prune` never deletes it.
3. Exactly **one namesake** of the same name and kind in the target. Since celigo-cli 2026.10.1 the kind is, for a connection, its adaptor `type` and, under it, its database (`rdbms.type`) or JDBC driver (`jdbc.type`); for an iClient, its `provider`; for a stack, its `type`. A same-named resource of another kind is not a namesake, and the refusal names its kind; a field one of the two lacks does not count. On-premise agents and MCP OAuth providers match by name alone.

A pin or a persisted binding is a choice already made, so it binds even when it names a resource of another kind — usually a binding an older release recorded by name, before databases were compared. The plan then says so in a `note:` naming both kinds and the `--map` that re-points it, on every promote while the mismatch stands.

None → the resource is `refused` and every dependent (its exports, imports, the integration that registers it, the flows) is `blocked`, with both exits in the message: the pin, or `--create-connections`. Several namesakes → `refused`; a pin decides.

`--create-connections` creates a credential-less **shell** in the target — the same name, type, and every non-secret field, with every masked `******` value replaced by the placeholder `REPLACE-IN-UI` (the platform rejects some connections when the field is missing outright), and every secret-named field or field the platform encrypts that holds anything else omitted, so a key typed into the source tree never reaches the target — and binds the dependents to it. Enter the real credentials once, either way the `note:` names: in the target's UI, or with the CLI:

```bash
celigo connections get <id> --profile <target> --format json > conn.json
# replace every REPLACE-IN-UI in conn.json
celigo connections update <id> --file conn.json --profile <target>
```

`connections set` patches only `name`, the debug window, and the credential-free concurrency settings, so it cannot reach a credential; `update` is the full-document `PUT`. Promote never writes the connection again after the shell — the binding is an id — so what you enter there stays.

A shell's own references resolve the same way (since celigo-cli 2026.10.1): its iClient, its on-premise agent or stack, and the connection it borrows concurrency from are matched by pin, by the persisted binding, or by one namesake, and recorded in the target file with a `map:` line each — but never created as shells of their own. One that resolves nowhere (no namesake, several, a pin the target lacks) is left out of the shell with a `note:` saying why: pin it in a dry run before the shell is created, or set it in the target along with the credentials. A shell that borrows from another shell of the same run is created after it; when two borrow from each other, one of the two references is left out, with a note.

The recommended discipline is the same as for [neutral trees](/cli/local-tree/neutral-trees.md): name connections the same in every environment, and the first promote maps them all by name with no flags.

## The first promote into an existing target

A target that already holds hand-built copies — the usual state of a production account that predates the tree — is where names bite. Promote's namesake pre-flight finds a same-name, same-type resource in the same place (the mapped integration, or account level) that is not in the map, and refuses to create a second one beside it. The refusal names `--adopt-by-name`, which maps that resource to the source file and updates it in place from then on. "Same type" includes the adaptor: an HTTP export is never adopted for a NetSuite export of the same name, nor refused on its account.

Run the first promote as a dry run, read the refusals, decide per resource — adopt, or rename one of the two — then promote with the flag. The pre-flight runs again right before each type's first `POST`, against a listing fetched in that run, so a namesake that appeared between the plan and the write (a colleague promoting the same commit, the UI) fails that create rather than producing a second copy. `--allow-duplicate-names` skips both listings.

Since celigo-cli 2026.10.1 `--adopt-by-name` also adopts the flows, tools, APIs, and syncs under an integration it adopts in the same run, and maps a target namesake to one source document only: a second same-named source document is created beside it instead of being written over the first one's copy. An Integration App's document in the target is never adopted, with the flag or without it: the create beside it is refused, naming the local rename.

## Event Streams topics

> Added in celigo-cli 2026.10.1.

Topics are content, not environment, so they travel as documents. Promote creates a topic the target lacks before the listeners and publishers bound to it, remaps their `topic._topicId`, and registers the mapped topics on the integration's target copy (`PUT /v1/integrations/<id>/topics/register`), as it registers connections and lookup caches. A topic is account-level, so its namesake is account-level too: the target topic holding the same `externalId`, whatever its name, before any same-named one. A same-named topic with a different `externalId` is another topic, and the create goes beside it. `--adopt-by-name` maps either kind. The refusal names the holder as the target calls it, and adopting it gives it the source topic's name:

```
refused: topics/orders.created/topic.json — target topic 'WMS orders' (6c2d3e4f5a6b7c8d9e0f1a2b) holds this topic's externalId 'wms-orders' at the same place and is not mapped to this file — pass --adopt-by-name to map it and update it in place (renaming it 'orders.created'), or change the externalId of one of the two
```

The plan refuses up front what the target would refuse halfway through:

* **A draft topic** (`"draft": true`). The platform deletes a draft 48 hours after creating it, so its target copy would vanish under the steps bound to it. Set `"draft": false` and push that to the source first.
* **A topic write the target's license rules out.** When the plan creates or updates a topic, promote reads the target's license once (`GET /v1/licenses?type=io`) and refuses what the platform would: no Event Streams entitlement, more creates than the license's topic limit (`topics.topicsAllowed`) leaves room for beside the topics the target already holds, or a `retentionMs` above its retention ceiling (`topics.retentionCeilingDays`). A license promote cannot read is a `note:`, not a refusal — the platform still decides.
* **Under `--prune`, a topic a target step still binds**, when that step is not one this run writes.

Two notes need you after the run. A listener subscribes only when its flow is switched from disabled to enabled, and promote never toggles a flow: a listener flow it creates enabled, switches to enabled, or gives a new listener is not reading its topic yet, and the note names the two commands that start it:

```
note: integrations/Orders/flows/Fulfill Orders/flow.json: flow 6b1c2d3e4f5a6b7c8d9e0f1a in 'sandbox' runs an Event Streams listener, which subscribes to its topic only when the flow is switched from disabled to enabled — promote created the flow enabled and never toggles one; to start the listener: `celigo flows set 6b1c2d3e4f5a6b7c8d9e0f1a disabled=true --profile sandbox`, then `celigo flows set 6b1c2d3e4f5a6b7c8d9e0f1a disabled=false --profile sandbox`
```

And a listener with `startPosition: "earliest"` that newly subscribes to a topic adopted from the target replays every message that topic still retains before any new one (a topic promote creates holds none). Set `topic.startPosition` to `"latest"` first when that backlog is not yours to process. See [topics](/cli/commands/topics.md) for the topic commands.

## Prune deletes

Resources removed from the tree leave their target copies in place and are listed as `note:` lines. `--prune` plans them as `delete` — only what the map holds, never anything else in the target — under the same plan-time lock as `push --prune` (a `GET`, refused when the copy changed since the last promote, `--force-remote` overrides, re-checked right before each `DELETE`), and asks `Delete N target copies from '<name>'? [y/N]`; `-y` skips the prompt, and declining skips the deletes while the rest is promoted. The map entry is dropped on success (`deleted` or `already gone`). A resource parked in an ignored folder or left unfinished by an interrupted pull or push is not a removal, so it is never pruned. Promote never deletes a connection or another per-environment resource in the target either — a shell it created included — since other bindings may share it.

Since celigo-cli 2026.10.1 the updates are re-checked right before their `PUT` as well, so a target copy edited while the prompt waited is refused, and a run whose target file another promote of the tree — or a `git pull` — rewrote meanwhile sends nothing:

```
.celigo/targets/sandbox.json changed since this promote's plan was computed — another promote of this tree (or a git pull) wrote it while this one waited; nothing was sent. Promote again to plan against the file as it is now.
```

> ⚠️ **`promote --prune` deletes resources from the target account.** Read the `delete` lines of `celigo promote --to <name> --prune --dry-run` first.

The deletes run after the updates: when the source removes a flow and the integration alias that named it in one commit, the alias leaves the target integration first, and the flow's `DELETE` goes through (the platform refuses to delete a flow an alias still names). An alias added in the target's UI that still names a pruned flow fails that delete with the way out — remove the alias there, then prune.

## Interrupting a promote

Ctrl-C once finishes the in-flight request and stops with the report (exit `130`); twice aborts. Creates are interrupt-safe against the target: the intent (type, name, path, time, owner) is durable in the target file before the `POST` and cleared on landing, so the next promote adopts what an interrupted run created — by name, time window, and content — as `recovered` instead of creating a second copy, or refuses with a sentence when it cannot tell. A copy the interrupted run's own `PUT` already brought up to date is recognized on resume, so a crash never demands `--force-remote` for promote's own writes.

## An Integration App installed in both environments

Promote never writes, deletes, or adopts an Integration App's documents (`_connectorId`): the app's connector code owns them, and every install has its own ids. They are listed as `withheld:`, and since celigo-cli 2026.10.1 the line names the way forward. When the target has no install of the app, install it there (`celigo connectors install`) and promote your own resources around it. When both environments have it — the app's flows were changed in the sandbox, and production needs the same change — make the change again in a tree of the target's own:

1. **Pull the target into its own tree**, under the target's profile: `celigo pull --profile prod --dir ./acme-prod`, with `--integration <id>` (the target install's integration id) to pull just the app. The source tree cannot carry the change: the two installs share no ids, and a tree is bound to the account it was pulled from. Pass `--profile prod` on every command against the new tree.
2. **Make the same change there and review it**: `celigo diff --profile prod --dir ./acme-prod`, then `celigo push --dry-run --allow-connector-resources --profile prod --dir ./acme-prod` (without the flag the plan lists the app's edits as `withheld:`). Redo the edit by hand — never copy the edited file over from the sandbox tree, whose documents name the sandbox's exports, imports, connections, and install. The sandbox change's own git diff is the checklist.
3. **Push it**: `celigo push --allow-connector-resources --profile prod --dir ./acme-prod` sends the app's edited flows, exports, and imports, and nothing else of the app's. A field the app locks is kept by the platform: the item fails naming it, and the file keeps your edit (`celigo pull --force <path>` takes the server's value back). Push needs full mode, its `--dry-run` included.
4. **Save the app's settings through the app**: set the values in the production tree's `integration.json` — its field names differ per install, so find each one there, never copy the sandbox's — and add `--allow-connector-settings` to the push, which saves them through the app's connector code and reads each one back. One field at a time, `celigo integrations settings <id> --profile prod` lists the target install's field names and `celigo integrations persist-settings <id> <fieldName>=<value> --profile prod` saves one.

Since celigo-cli 2026.10.1 promote also refuses to update or prune a target copy an Integration App manages even when the target file maps it — an adoption by name an earlier release recorded — and names the entry to remove from the target file. See [What push withholds](/cli/local-tree/push.md#what-push-withholds) for the Integration App flags.

## What promote does not do in this version

* **Neutral trees** (`pull --neutral`) are not promotable; promote works from a standard tree. The [one-branch-per-environment](/cli/local-tree/neutral-trees.md#one-branch-per-environment) setup is their alternative.
* **Integration Apps.** An app's documents (`_connectorId`) are `withheld`; install the app in the target and promote your own resources, or change the target's install from its own tree ([An Integration App installed in both environments](#an-integration-app-installed-in-both-environments)).
* **Celigo Storage** is pull-only in the tree and skipped by promote.
* **Lookup cache entries** stay behind: promote creates or updates a cache's document in the target, never its `data.json`. Load the target's entries with `celigo lookup-caches put-data <target cache id> --file <data.json> --profile <target>`, or let the flows that fill the cache run there.

## Day to day

```bash
celigo pull --dir ./acme                              # from the source profile
# edit, commit, review, push to the source as usual …
celigo promote --to sandbox --dir ./acme --dry-run    # what would change in the sandbox
celigo promote --to sandbox --dir ./acme              # do it; commit .celigo/targets/sandbox.json
celigo diff --target sandbox --dir ./acme             # sandbox copies vs what promote would send
```

`diff --target` only reads the target, so it runs in whatever mode the target profile is in — a read-mode production profile on a reviewer's machine serves it, and `CELIGO_MODE=read` does not stop it. Promote itself needs full mode. The CI job that promotes on merge, with the target's own token, is in [Teams, git, and CI](/cli/local-tree/workflow.md#promote-on-merge-two-tokens).

## Related

* [Push changes](/cli/local-tree/push.md) — the same plan, the same guards, against the tree's own account.
* [Lint and resolve](/cli/local-tree/lint-and-resolve.md) — the logic rules the plan runs.
* [Status and diff](/cli/local-tree/status-and-diff.md) — `diff --target <name>`.
* [Neutral trees](/cli/local-tree/neutral-trees.md) — the other multi-environment model.
* [Teams, git, and CI](/cli/local-tree/workflow.md) — the promote job and the two tokens.
* [connections](/cli/commands/connections.md) — `get` and `update` for entering a shell's credentials.
