> 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/push.md).

# Push changes

`celigo push` writes a [local tree's](/cli/local-tree.md) changes back to the account: a `PUT` for each modified document, a create for each untracked file that has no `_id`, and — only with `--prune` — a delete for each tracked file you removed. It is plan-first: the change set prints before anything is written, `--dry-run` stops there, and every guard the push applies runs while the plan is built, so the plan is the truth about what will be sent.

**Required:** full mode, `--dry-run` included. Every push names its target first on stderr: `celigo: profile 'acme-prod' → api.integrator.io`.

**Signature**

```bash
celigo push [refs...] [flags]
```

**Arguments**

| Argument    | Type                                        | Required | Description                                                                      |
| ----------- | ------------------------------------------- | -------- | -------------------------------------------------------------------------------- |
| `[refs...]` | file path, link path, `type:id`, or bare id | No       | Narrow the push to these resources. Without refs, every local change is planned. |

**Flags**

| Flag                          | Type    | Default | Description                                                                                                                                                                                                                                                                                                                                                                                 |
| ----------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dir <path>`                | string  | `.`     | Tree root directory. Since celigo-cli 2026.10.1 a blank value — what an unset shell variable produces — is refused instead of pushing from the working directory.                                                                                                                                                                                                                           |
| `--dry-run`                   | boolean | `false` | Compute and print the plan without writing anything. Exits `1` when any item is refused.                                                                                                                                                                                                                                                                                                    |
| `--prune`                     | boolean | `false` | Delete resources on the server whose local files were removed. Asks for confirmation.                                                                                                                                                                                                                                                                                                       |
| `--allow-dependencies`        | boolean | `false` | Also push resources owned by an integration outside this tree's scope (pulled as read-only context by default).                                                                                                                                                                                                                                                                             |
| `--force-remote`              | boolean | `false` | Skip the remote-change (`lastModified`) check and overwrite the server copy.                                                                                                                                                                                                                                                                                                                |
| `--no-create`                 | boolean | `false` | Refuse to create resources for new files — a CI safety. `--dry-run --no-create` counts them as refused and exits `1`.                                                                                                                                                                                                                                                                       |
| `--allow-connector-resources` | boolean | `false` | Also push edited Integration App flows, exports, and imports. Since celigo-cli 2026.10.1, a field the app locks — the platform keeps its own value and answers `200` — fails the item, named, and your edit stays in the file. An app's `integration.json` is never `PUT`; see `--allow-connector-settings`.                                                                                |
| `--allow-connector-settings`  | boolean | `false` | Save the settings values edited in an Integration App's `integration.json` through the app's connector code, as the UI's Save does: one persistSettings call per store, then a read-back of every field. Values only. Reads the integration to plan, so `--dry-run` with this flag needs a token. See [Integration App settings](#integration-app-settings). Added in celigo-cli 2026.10.1. |
| `--allow-account-mismatch`    | boolean | `false` | Push to a different account than the tree is bound to (host or token owner) anyway.                                                                                                                                                                                                                                                                                                         |
| `-y, --yes`                   | boolean | `false` | Skip the delete confirmation prompt.                                                                                                                                                                                                                                                                                                                                                        |
| `--concurrency <n>`           | integer | `8`     | Requests in flight at once (1–16). `CELIGO_CONCURRENCY` sets the default. Creates still run in dependency order, deletes dependents-first.                                                                                                                                                                                                                                                  |
| `--force-lock`                | boolean | `false` | Take the tree lock even if another pull or push appears to hold it.                                                                                                                                                                                                                                                                                                                         |
| `--no-verify`                 | boolean | `false` | Skip the one `GET` after each update that detects another writer overwriting it right behind the `PUT`.                                                                                                                                                                                                                                                                                     |
| `--allow-duplicate-names`     | boolean | `false` | Create a resource even when an untracked one of the same name already exists on the server. Skips the pre-`POST` namesake listing.                                                                                                                                                                                                                                                          |
| `--allow-lint-errors`         | boolean | `false` | Send a document the logic lint refuses. The rules still run on every planned document and every finding is printed; an error prints as `lint: … (error allowed by --allow-lint-errors)` instead of refusing the item. See [The logic lint gate](#the-logic-lint-gate). Added in celigo-cli 2026.10.1.                                                                                       |

**Example**

```bash
# See the exact change set; exit 1 if anything would be refused
celigo push --dry-run

# Push every local change
celigo push

# Push one resource, and the deletes for files you removed
celigo push "integrations/Shop/flows/Order Sync/flow.json"
celigo push --prune

# A CI deploy: never create, machine-readable report
celigo push --yes --no-create --format json

# List, then save, the settings values edited in an Integration App's integration.json
celigo push --dry-run --allow-connector-settings "integrations/Shopify - NetSuite/integration.json"
celigo push --allow-connector-settings "integrations/Shopify - NetSuite/integration.json"
```

## The plan

The plan has three kinds of line. Items that will be sent (`create`, `update`, `delete`); `refused: <path> — <reason>` for every item the real push would fail on; and `withheld: <path> — <reason>` for what push leaves alone by design, with the flag that would send it. Since celigo-cli 2026.10.1, logic lint warnings follow as `lint:` lines (see [The logic lint gate](#the-logic-lint-gate)); notes about moved, parked, and unfinished resources print on stderr before the plan (see [Moved, parked, and unfinished resources](#moved-parked-and-unfinished-resources)).

```
Plan: 1 to update, 2 refused, 1 withheld
  update  flow:6a9b…  integrations/Shop/flows/Order Sync/flow.json
  refused: integrations/Shop/flows/Wrapped/flow.json — integrations/Shop/flows/Wrapped/flow.json: responseMapping.fields at /pageProcessors/0/responseMapping/fields is an object, not an array — the platform persists this as fields:[{}] and erases the step's mappings. Use {"fields":[…],"lists":[…]}.
  refused: flows/Foreign/flow.json — flows/Foreign/flow.json: _integrationId 6a4b… is outside this tree's scope (integration 6a9b…) — a scoped tree never writes into another integration. Move the file under integrations/Shop/ and drop the field, re-pull with `--integration 6a4b…` or without a scope to work on it there, or pass --allow-dependencies to write it anyway.
  withheld: integrations/Shopify - NetSuite/flows/Sync/flow.json — managed by an Integration App; a raw PUT bypasses the app's connector code. Pass --allow-connector-resources to push it anyway (flows/exports/imports only); for its settings, run `celigo integrations persist-settings 6344…`
```

A refused create also blocks everything that depends on it, transitively, so a refused create never leaves half a graph behind:

```
Plan: 1 to create, 2 refused
  create  import    imports/Post Orders/import.json
  refused: exports/New Orders/export.json — unresolved reference(s): /_connectionId → connection 'connections/_missing.json' (the referenced resource is neither tracked in this tree nor created in this push)
  refused: flows/New Sync/flow.json — blocked: /pageGenerators/0/_exportId names exports/New Orders/export.json, whose create is refused — fix that file first, then push again
```

`--dry-run` then prints `Dry run — nothing was written; N item(s) refused (exit 1): <paths>` and **exits 1**, so a CI check cannot approve a half-merged tree or a payload the push would refuse; `--format json` carries the reason as `items[].problem`. Withholding is not a failure: a plan that is only withholdings ends with `Nothing to push — N change(s) withheld (see above).` and exits `0`.

## What push refuses

These refusals have no override. Fix the file, or re-pull it.

| Refusal                                                                                                                                                                                                                                                                                                                      | Why                                                                                                                                                                                                              |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A document whose `_id` is not the tracked resource's — a copied, symlinked, or overwritten file.                                                                                                                                                                                                                             | The server ignores a mismatched body `_id` and would overwrite the other resource.                                                                                                                               |
| A `connections`, `iclients`, or `mcp-oauth-providers` document still holding `"******"`.                                                                                                                                                                                                                                     | A `PUT` with the mask would destroy the credential. Type the real value, use the group's `set` for its safe fields, or re-pull.                                                                                  |
| A create whose document carries the mask — four or more asterisks and nothing else — anywhere, on every type. Since celigo-cli 2026.10.1.                                                                                                                                                                                    | A new resource has no secret behind the mask, so the asterisks would become its credentials: a copied stack folder, a masked sample header in a new export. Replace each with the real value, or omit the field. |
| A value that is not a string — an object, array, number, or boolean — in a field the platform encrypts. Since celigo-cli 2026.10.1.                                                                                                                                                                                          | An encrypted field holds only a string, and the platform fails on any other value instead of refusing it. Send a string (`"123"` for a number, not `123`), or `null` to clear the field.                         |
| A draft topic (`"draft": true`). Since celigo-cli 2026.10.1.                                                                                                                                                                                                                                                                 | The platform deletes a draft 48 hours after creating it, taking the topic of every step bound to it. Set `"draft": false` to keep it.                                                                            |
| A `--prune` delete of a topic that an export or import still binds. Since celigo-cli 2026.10.1.                                                                                                                                                                                                                              | The platform refuses that delete (`422 dependencies_not_deleted`), so `--force-remote` does not lift it. See [Event Streams topics](#event-streams-topics).                                                      |
| A lookup cache `data.json` entry the platform would refuse: a top-level array value, an object value with its own top-level `key`, an entry with no `value` (`null` stores an empty one), a key outside 1–255 characters or holding a tab, newline, carriage return, or NUL, or a duplicate key. Since celigo-cli 2026.10.1. | One such entry fails the platform's whole upsert batch.                                                                                                                                                          |
| The `responseMapping` object wrapper — `fields` or `lists` as an object instead of an array — on a create or update, at any depth (`routers[].branches[].pageProcessors[]` included), and `fields: null`. On a create, the post-corruption `fields: [null]` and `[{}]` too.                                                  | The platform accepts the shape, answers `200 OK`, and erases the step's mappings. `celigo lint` reports it as an error for the same reason.                                                                      |
| A Celigo Storage item.                                                                                                                                                                                                                                                                                                       | Storage is pull-only; the [`celigo storage`](/cli/commands/storage.md) commands write it.                                                                                                                        |
| A renamed script file without `--prune`.                                                                                                                                                                                                                                                                                     | Ambiguous between a rename and a delete plus a create. A script is renamed by its `name` field in `script.json`.                                                                                                 |
| A create of a connection, iClient, MCP OAuth provider, stack, or on-premise agent in a [neutral tree](/cli/local-tree/neutral-trees.md).                                                                                                                                                                                     | Per-environment resources are created with their own commands, then pulled.                                                                                                                                      |

These refusals name their exit in the message:

* **A file that is not a JSON object**, and a `script.js` or `instructions.md` holding git conflict markers: `<path>: unresolved git merge conflict (conflict markers at line N)`. Text has no parser to trip over `<<<<<<<`, so the scan is explicit — otherwise a half-merged script would be pushed as the script's code.
* **A vanished companion file** — a deleted `instructions.md` or `script.js`. A `PUT` that omits the prompt would clear it; `celigo pull` restores the file.
* **A document outside a scoped tree's integration.** See [Scoped trees](#scoped-trees); `--allow-dependencies` overrides.
* **A create claiming an Integration App's `_connectorId`.**
* **A new file whose folder push cannot honor** — an unknown flow or API group (`'<Group>' does not match a flow group of this integration — create the group first (flowGroupings on the integration), or move the file to flows/`), a folder no tracked integration owns, an `instances/` folder with no tracked parent, a file nested too deep, or a grouped flow under an integration created in the same push.
* **A reference that resolves to nothing**: `unresolved reference(s): <pointer> → <type> '<id>' (the referenced resource is neither tracked in this tree nor created in this push)`, or, when `push <refs>` left the new file out of the plan, `… is a new file that this push's refs left out — add it to the refs, or push without refs`.
* **A `--prune` delete whose server copy changed since the pull** (see [Prune deletes](#prune-deletes)).
* **A new resource whose untracked namesake already exists on the server** (see [Creates](#creates)); `--allow-duplicate-names` overrides.
* **A logic lint error** in a new or changed file (see [The logic lint gate](#the-logic-lint-gate)); `--allow-lint-errors` sends it anyway.
* **An Integration App settings save** that changes anything but settings values, removes a value, or sets one to a mask (see [Integration App settings](#integration-app-settings)).

## What push withholds

Withheld items are left alone by design and do not fail the push:

* **Resources another integration owns**, pulled into a scoped tree as reference context. `--allow-dependencies` sends them — edits and `--prune` deletes alike, so removing a borrowed folder from a checkout never deletes the owner's documents without the flag.
* **Integration App documents** (`_connectorId`). Edits are withheld, `--prune` never deletes them, and an app's `integration.json` is never `PUT`. `--allow-connector-resources` pushes edited app flows, exports, and imports, with `_connectorId` stripped. The settings values you edit in the app's `integration.json` are saved through the app's connector code — with `--allow-connector-settings` (see [Integration App settings](#integration-app-settings)), or one at a time with [`celigo integrations persist-settings`](/cli/commands/integrations.md#celigo-integrations-persist-settings). Since celigo-cli 2026.10.1 the `withheld:` line names the flag:

  ```
  withheld: integrations/Shopify - NetSuite/integration.json — an Integration App's integration.json is never PUT — pass --allow-connector-settings to save the settings values you edited through the app's connector code (the UI's Save), or save them one at a time with `celigo integrations persist-settings 5f83a9b2c7d3e8f1a2b3c4d5`; the rest of the file is the app's
  ```

## The logic lint gate

> Added in celigo-cli 2026.10.1.

`push` runs the six [logic rules](/cli/local-tree/lint-and-resolve.md#logic-rules) of `celigo lint` — `hook-function`, `script-syntax`, `lookup-reference`, `router-reference`, `handlebars-syntax`, `topic-binding` — on every planned create and update, at plan time. An error refuses the item like any other guard, so `push --dry-run` exits `1`, and a dependent of a refused create is `blocked:` as usual:

```
refused: exports/Get Orders/export.json — exports/Get Orders/export.json: /hooks/preSavePage/function — function 'preSave' is not defined in scripts/Hooks/script.js (it defines preSavePage) — fix the name here or add the function to the script (lint hook-function; pass --allow-lint-errors to send it anyway)
```

A warning prints after the plan as `lint: <path> <pointer> <message>`, and the item is sent; `--format json` lists the warnings in `lintWarnings[]`, each with its `rule` and `level`.

**Severity follows who introduced the finding.** A finding is an error only when it sits in, or was read from, a file that is new or changed since the pull — a hook's script source counts. A finding among files that are all unchanged since the pull already exists like that on the account, so it is a warning: the gate never refuses a push for what the account already holds.

**A script is held to the documents that run it.** When the plan carries a script, every tracked document whose handler names it is read too, and a hook the new source no longer satisfies refuses the script: `this script leaves exports/Get Orders/export.json broken at /hooks/preSavePage/function — …`.

**`--allow-lint-errors` lifts the refusal, not the lint.** The rules still run and every finding is printed; an error prints as a `lint:` line ending `(error allowed by --allow-lint-errors)`, and the item is sent. Schema findings (`type`, `enum`, `unknown-field`) never gate a push: the published specs are stricter than the API. [Promote](/cli/local-tree/promote.md) runs the same rules against its target.

## Moved, parked, and unfinished resources

A folder you moved or renamed locally is a **move**, never a delete plus a create: its edits push from the path where it now sits, and `push --prune` never deletes the resource. A folder name follows the resource's own `name` and is not stored, so the move itself is not pushed, and the plan says so on stderr:

```
moved locally: exports/Get Orders/export.json → exports/Orders/export.json — the move itself is not pushable (a folder name follows the resource's own "name" and is not stored), and export:5f83a9b2c7d3e8f1a2b3c4d5 is NOT deleted. Edits push from the new path; rename via "name" in the document, or run `celigo pull` to move the folder back
```

Since celigo-cli 2026.10.1, a moved resource goes through every plan-time guard at its new path — document refusals, the logic lint, references, the integration scope — and a path placeholder in another document may name it where it now sits (`"_exportId": "exports/Orders/export.json"`). A renamed script folder is a move too. In a [neutral tree](/cli/local-tree/neutral-trees.md), whose documents carry no `_id`, a folder moved without edits is paired with its resource by content; a folder moved *and* edited cannot be told from a delete plus a create, and the plan says so under the lines involved. See [Push and diff in a neutral tree](/cli/local-tree/neutral-trees.md#push-and-diff-in-a-neutral-tree).

Since celigo-cli 2026.10.1, a **parked** resource — a tracked folder moved into a `.celigoignore`d folder — is never written, even when edited there, and never deleted under `--prune`:

```
parked: exports/Get Orders/export.json → integrations/Sandbox/exports/Get Orders/export.json — in an ignored folder; not pushed, not deleted under --prune, and pull leaves it parked (manifest path unchanged)
```

An **unfinished** resource — one an interrupted pull or push was renaming, untracking, or adding to the tree (see [Interrupted pulls and pushes](/cli/local-tree.md#interrupted-pulls-and-pushes)) — is neither updated, pruned, nor created until `celigo pull` finishes it, and naming it as a ref never reports it `up to date`. A plan with nothing else to send says so:

```
unfinished pull: exports/Get Orders/export.json → exports/Orders/export.json (an interrupted `celigo pull` was renaming it — run `celigo pull` to finish) — export:5f83a9b2c7d3e8f1a2b3c4d5 is not pushed, and not deleted under --prune
Nothing to push — 1 resource left unfinished by an interrupted pull or push (see above); run `celigo pull` to finish, then push again.
```

## Creates

An untracked file with no `_id` is a create. Author it as `<typedir>/<Name>/<doc>.json` (folder form) or `<typedir>/<Name>.json` (quick form); the write-back normalizes to the folder form. A new script is a `.js` dropped in `scripts/` — `scripts/<Name>.js`, or `scripts/<Name>/script.js` with an optional `script.json` beside it.

**New files reference each other by relative path.** A placeholder names the file of a tracked resource or of another new file in the same push, at any id-bearing position:

```json
{ "name": "New Orders", "_connectionId": "connections/_new-store.json" }
```

Push resolves the placeholders to server ids and runs creates in dependency rounds — a round is every remaining create whose dependencies have been attempted — so an import is never `POST`ed before its export's id exists. The order is derived from the documents on every push and never stored, which is what lets a git merge that adds a flow and its new exports push correctly. A reference cycle (a script that calls the export that hooks it, an integration alias naming one of its own flows) is created without the deferred half and repaired by a disclosed fixup `PUT`; a cycle within one tier is a hard error naming the nodes.

**Location decides ownership.** A flow, sync, tool, or builder-mode API authored inside `integrations/<Name>/…` inherits `_integrationId` from its location — plus `_flowGroupingId` in a flow-group subfolder, `_apiGroupingId` in an API-group subfolder, `_abstractFlowId` in an `instances/` folder — disclosed as a side effect. An explicit field in the document always wins. What the platform cannot own inherits nothing: a connection, a script-mode API, a lookup cache, a topic, or an MCP server authored inside an integration folder is created account-global, and the write-back moves the file to its pool; the plan says so before anything is sent.

**Namesakes.** Immediately before each `POST`, the type's current listing (one per created type per push) is checked for an untracked namesake — the only way to see the same new file pushed from another clone of the tree. One holding exactly this document, or one in the same integration, refuses the create:

```
import 'Post Orders' already exists on the server (6a9b…) holding exactly this document, and this tree does not track it — another clone of this tree pushed the same file? Run celigo pull to track it, then remove the draft; or rename the new one (change "name" in imports/Post Orders/import.json); pass --allow-duplicate-names to create it anyway.
```

A pooled type's namesake with different content is a note, and the create proceeds — names are not unique account-wide. `--allow-duplicate-names` skips the check and its listings.

**After a create, the file is the server's copy.** The write-back renders the response the way `pull` does, `_id` included, at the resource's canonical path (`scripts/New Hook.js → scripts/New Hook/script.js (_id … written back)`), and the manifest records the server's hash.

## Updates and the optimistic lock

A modified tracked document is a `PUT` of the whole file (minus the volatile fields the tree strips). The API has no conditional write, so the lock is best-effort by construction: one `GET` immediately before each `PUT`, comparing the `lastModified` token recorded at pull time. A mismatch means someone changed the server copy since your pull:

```
failed  flow:6a9b…  integrations/Shop/flows/Order Sync/flow.json  — remote changed since pull — inspect with `celigo diff integrations/Shop/flows/Order Sync/flow.json`; then `celigo pull --force integrations/Shop/flows/Order Sync/flow.json` to take the server copy, or `celigo push --force-remote integrations/Shop/flows/Order Sync/flow.json` to overwrite it
```

Three exits, all per file: `celigo diff <path>` shows the two versions, `celigo pull --force <path>` takes the server copy of that one resource, `celigo push --force-remote <path>` overwrites it. The lock is lenient where nothing stands to be lost, and strict everywhere else:

* A server copy that already holds exactly the document you are about to send — an interrupted push whose `PUT` landed, a lost response — is adopted as `up to date`; nothing is sent. Since celigo-cli 2026.10.1 a secret typed in place of a `******` mask is never counted as held: the server echoes the mask whatever it holds, so the document is sent.
* A token that moved while the server copy still renders to exactly what was pulled is not a change: `connections enable-debug`, a flow run, or the UI's debug toggle bump `lastModified` and touch only volatile fields, so the write goes through. A lookup cache whose `data.json` the tree tracks is the exception: its token is all that vouches for the data, so since celigo-cli 2026.10.1 a moved token refuses any edit of the cache, unless it is this tree's own unfinished data sync (see [Interrupting a push](#interrupting-a-push)).
* Where a token is missing on either side — a sync document has no `lastModified` at all — the rendering is the lock: the write goes through when the server copy still renders to what was pulled and is refused as `remote changed since pull` otherwise. The lock never falls open; a missing token beside a dirty data companion (cache data, a sync's datasets) refuses as `cannot verify the server copy`.

**After a push, the file is the server's copy.** Updates and creates alike write the server's response back through the same rendering `pull` uses, and the manifest records that hash — so `status` is clean *because* the tree is server-identical, the pull right after a push is a no-op, and a secret you typed is masked again immediately. Anything the server silently dropped is said on stderr:

```
note: integrations/Shop/integration.json: server dropped /_registeredConnectionIds (use `celigo integrations register-connections`)
```

`_registeredConnectionIds`, `_registeredLookupCacheIds`, and `_registeredTopicIds` are the classic case — a `PUT` ignores them; registration goes through [`celigo integrations register-connections`](/cli/commands/integrations.md#celigo-integrations-register-connections), `register-lookup-caches`, and [`register-topics`](/cli/commands/integrations.md#celigo-integrations-register-topics). A pushed `name` change moves the resource's folder, links included.

**An integration's aliases register what they name first.** Since celigo-cli 2026.10.1, push makes the one registration an integration's `aliases[]` need, because the platform refuses an alias whose target is not registered to the integration. The connection or lookup cache an alias names — or the connection behind an export or import alias's step — is registered before the `PUT` that carries the alias. A new integration is created without its aliases, registers what they name, then gets them in a follow-up `PUT`. The plan says so in a `note:` line (`deferredAliases` and `aliasRegistrations` in `--format json`); a registration that fails refuses the write and names what was already registered. Anything else in the `_registered*Ids` lists is still the `register-*` commands' to apply, and a change only there is never taken for a remote change by the lock.

**A connection update carries the server's `debugDate`.** A connection's `debugDate` is the one debug timer a full `PUT` erases when omitted, so push copies the server's current value into every connection update; the file never holds it and `diff` stays clean. Updating an abstract flow prints a note that its instances inherit the change silently.

**A lost update is made visible.** After each document `PUT`, one `GET` re-reads the server copy. A document that no longer matches what was just written means another writer landed behind the `PUT`:

```
warning: exports/Get Orders/export.json was overwritten by another writer right after this push (server now differs) — run `celigo diff exports/Get Orders/export.json`, then push again to re-apply or `celigo pull --force exports/Get Orders/export.json` to take theirs
```

The manifest then records the **server's** hash, so `status` shows the file `modified` instead of a clean tree that differs from the account. `--no-verify` skips that `GET`.

## Integration App settings

> Added in celigo-cli 2026.10.1.

An Integration App's `integration.json` is never `PUT`: its `settings` are the app's store, and the app's connector code validates them and may reconfigure the app's flows. With `--allow-connector-settings`, push turns your edit of that file into the app's own save — what the UI's Save and [`celigo integrations persist-settings`](/cli/commands/integrations.md#celigo-integrations-persist-settings) do — and the plan lists every field it will send:

```
Plan: 1 to update
  update  integration:5f83a9b2c7d3e8f1a2b3c4d5  integrations/Shopify - NetSuite/integration.json  (settings, through the app's connector code)
          store 6a1b2c3d4e5f6a7b8c9d0e1f  general_6a1b2c3d4e5f6a7b8c9d0e1f_autoAssignInventoryDetail: true → false
```

* **It reads the integration to plan**, after the identity check, so `--dry-run` with the flag needs a token. The save is judged against the copy you pulled: a server copy that moved since the pull is refused, naming `celigo diff <path>`, `celigo pull --force <path>`, and `--force-remote`. With `--force-remote`, the file's values are saved over the server's copy, the fields matched by name; where the server's copy differs outside the values — an option list the app refreshed, fields an app upgrade added or dropped — is listed in a `note:` under the plan line and after the push, and left as the server holds it.
* **Only values travel.** A change to a label, an option list, the form's fields, or anything else in the file refuses the item, naming where, and so do a value removed from its field (a save cannot remove one) and a changed value that is a mask (`******`, which the app would store as the setting itself). A select that stores a value and a label is sent as `{id, label}`, the label read from the field's own options; a value those options do not list is refused with the `persist-settings` command that saves it. A number or `true`/`false` in a field the form declares as text is sent as text — a bare number exactly as the file spells it (`5.50` as `"5.50"`).
* **Every field is read back.** The saves run before the other updates, one persistSettings call per store, and the integration is then read again and every field checked, because the platform answers success for a value it dropped. The file becomes the server's copy. A field the app did not keep fails the item and stays edited in the file, so the next push sends just that field; the failure lists what was saved and what was not, and a store the run did not reach after an earlier store's save failed is reported as `not sent`.
* **A neutral tree refuses the save.** An app's settings belong to one install; push them from a standard tree of that environment. [Promote](/cli/local-tree/promote.md) never writes an app's documents.

Without the flag, the edit is `withheld:`, and the line names the flag (see [What push withholds](#what-push-withholds)).

## Event Streams topics

> Added in celigo-cli 2026.10.1.

Topics push like any other document, in dependency order: a new topic is created before the listeners and publishers bound to it, and under `--prune` a topic is deleted after them, since deletes run dependents first.

* **Server-managed fields stay behind.** A topic's `_integrationId`, `_connectorId`, `_sourceId`, `_templateId`, `_userId`, `createdAt`, `deletedAt`, and `draftExpiresAt`, and a create's `__integrationIds`, are never sent. A topic is registered to an integration through `_registeredTopicIds`, which a `PUT` ignores: use [`celigo integrations register-topics`](/cli/commands/integrations.md#celigo-integrations-register-topics) (saving a flow with a step bound to the topic also registers it). A note names that command for any registration push left out.
* **A listener starts only when its flow is toggled.** The platform subscribes a listener to its topic only when the listener's flow is switched from disabled to enabled, and push never toggles a flow. When a push creates a listener flow enabled, switches one to enabled, or gives one a listener the same push created, a note names the two commands that start it:

  ```
  note: integrations/Orders/flows/Fulfill Orders/flow.json: flow 5f83a9b2c7d3e8f1a2b3c4d5 runs an Event Streams listener, which subscribes to its topic only when the flow is switched from disabled to enabled — push created the flow enabled and never toggles one; to start the listener: `celigo flows set 5f83a9b2c7d3e8f1a2b3c4d5 disabled=true --profile acme-corp-production`, then `celigo flows set 5f83a9b2c7d3e8f1a2b3c4d5 disabled=false --profile acme-corp-production`
  ```
* **A draft topic is refused**, with no override. The platform deletes a draft 48 hours after creating it, taking the topic of every step bound to it; set `"draft": false` to keep it:

  ```
  refused: topics/orders.created/topic.json — topics/orders.created/topic.json is an unconfirmed draft topic ("draft": true) — the platform deletes a draft 48 hours after creating it, taking the topic of every step bound to it; to keep it, set "draft": false and push it to its own account, or remove it from the tree
  ```
* **A `--prune` delete of a topic a step still binds is refused** at plan time. The platform refuses to delete a topic an export or import binds through `topic._topicId` (`422 dependencies_not_deleted`), so `--force-remote` does not lift the refusal. A step this push creates or updates is judged from its file; a step outside the push is found with one `GET /v1/topics/<id>/dependencies` per planned topic delete, so `push --dry-run --prune` needs a token. A step the push deletes or repoints does not count, and a topic already deleted on the server is `already gone`:

  ```
  refused: topics/orders.created/topic.json — export 'Orders Created Listener' (5f83a9b2c7d3e8f1a2b3c4d5) still binds this topic (topic._topicId) outside this run — the platform refuses to delete a topic a step still binds (422 dependencies_not_deleted); delete or repoint it first (`celigo topics dependencies 6a1b2c3d4e5f6a7b8c9d0e1f` lists every binder), or restore it with `celigo pull --force topics/orders.created/topic.json`
  ```

## Prune deletes

Without `--prune`, nothing is ever deleted and no confirmation prompt appears. With it, every tracked file you removed is planned as a `delete`, held to the same lock as an update: one `GET` per planned delete at **plan time**, so `--dry-run --prune` reaches the network and needs a token that can read the account. A server copy modified since the pull is refused:

```
refused: <path> — remote changed since pull — someone modified the server copy after the last pull, so deleting it would discard their work; run `celigo pull --force <path>` to restore the file and see the change, or `celigo push --prune --force-remote <path>` to delete it anyway
```

The push asks `[y/N]` before deleting; `-y` skips the prompt, and declining skips the deletes while the rest of the plan is sent. Since celigo-cli 2026.10.1 the question counts only the deletes the plan will send: a delete the plan refused is not asked about. The lock is re-checked right before each `DELETE`; `--force-remote` skips both checks. Deletes run tier by tier with dependents first (instance flows before their parent), a `DELETE` that finds the resource already gone is reported `already gone`, and a tracked file that is back on disk by the time its `DELETE` runs — a `celigo pull` during the prompt restores missing files — fails the item rather than deleting a resource the tree holds again.

> ⚠️ **`--prune` deletes resources from the account.** Run `celigo push --dry-run --prune` first and read every `delete` line. In a scoped tree, deleting a flow leaves its steps unreferenced; the push names them (`note: no longer referenced by any tracked resource in this tree: …`) so you can delete their files and push `--prune` again.

## Results and exit codes

One line per item, in plan order whatever order the requests completed in. The summary counts `created`, `recovered`, `updated`, `up to date`, `deleted`, `already gone`, `failed`, and `skipped`; in `--format json`, `results[].status` is one of `created`, `recovered`, `updated`, `up-to-date`, `deleted`, `deleted-gone`, `failed`, `skipped`, and every plan note and prompt is on stderr so stdout is only the report. Per-file failures never abort the rest.

| Exit code | When                                                                                        |
| --------- | ------------------------------------------------------------------------------------------- |
| `0`       | Every planned item landed, or the plan held only withholdings.                              |
| `1`       | Any item failed or was refused — `--dry-run` included — or the manifest could not be saved. |
| `130`     | Interrupted with Ctrl-C.                                                                    |

## Interrupting a push

Ctrl-C once finishes the requests already on the wire (up to `--concurrency` of them), saves what landed, reports every remaining item as `skipped` (`N items not attempted (interrupted) — push again to continue`), and exits `130`. Press again to abort at once. Either way, a re-run converges on the same server state as an uninterrupted push:

* **Updates and deletes are idempotent.** A `PUT` that landed before its token was recorded is adopted as `up to date`; a `DELETE` that finds the resource gone is `already gone`.
* **Creates record their intent first.** Before the `POST`, the manifest records the create's type, path, name, and time. On the next push, an untracked namesake the server stamped no earlier than that intent, holding exactly the document the create body carried, is adopted as `recovered` instead of created twice. A namesake whose content differs is refused by name and id — inspect it, delete it, remove the local file, or rename the local resource (its `name` field) and push again. Because the intent lives in the committed manifest, a teammate resuming from your commit reconciles the same way.
* **A lookup cache's data sync records its writes first.** Since celigo-cli 2026.10.1, before a cache's first write push records, in `.celigo/local/data-sync/<cache id>.json`, each key's value before the sync and the value it sends. Every data write moves the cache's `lastModified`, so without the record an upsert whose answer was lost would leave the cache refused as `remote changed since pull` on every later push. The next push finishes such a sync — `resuming the data sync an earlier push did not finish` — when the server's data holds only that sync's own writes since the pull and the cache's document is the pulled or the sent one; anything else is refused as before. The record is per machine: a clone of the commit has none, and its push is judged by the token alone.
* **Folder moves are journaled.** Since celigo-cli 2026.10.1, push records a folder move in `.celigo/local/pull-journal.json` before it makes it — a pushed `name` edit moving the resource's folder, a new resource's draft moving to its canonical folder. Killed before the manifest save, the resource is `unfinished` and `celigo pull` finishes it, never a delete beside a create. A write-back that failed after writing a new resource's document leaves its draft `unfinished`, and the next push adopts the resource instead of creating it twice; a draft changed since is refused. See [Interrupted pulls and pushes](/cli/local-tree.md#interrupted-pulls-and-pushes).

## Scoped trees

A tree pulled with `--integration` may write its own integration and everything the integration reaches, and never another integration's. A create or edited update whose document names another integration — in an explicit `_integrationId`, one inferred from the folder it sits in, or a new `integration.json` of its own — is refused at plan time, and its same-push children are `blocked`:

```
integrations/Consumer/flows/Escape/flow.json: _integrationId 6a…b1 is outside this tree's scope (integration 6a…a1) — a scoped tree never writes into another integration. Move the file under integrations/Consumer/ and drop the field, re-pull with `--integration 6a…b1` or without a scope to work on it there, or pass --allow-dependencies to write it anyway.
```

`--allow-dependencies` has one meaning — write into an integration this tree does not own — and lifts both that refusal and the withholding of borrowed dependencies. A create the integration will not reach (a stack nothing references, a flow with no integration) is legitimate authoring: push notes it and goes ahead. `celigo lint` reports the same refusals offline as `scope` errors.

## The account binding

Push settles the tree's identity before its first read of the account: the manifest's API host offline, then the token owner with one `GET /v1/tokenInfo`. A tree bound to another host or another account user is refused, however the profile was selected. `push --dry-run` runs the same check whenever a token is configured and the plan has items; without a token it plans offline and says so (`note: no API token configured — offline dry run: …`). A different profile *name* on the same account is only a stderr note, so an env-only CI job can push. `--allow-account-mismatch` is the only override. Since celigo-cli 2026.10.1, a `tokenInfo` answer that names no account (no `ownerUser`, as for an OAuth sign-in whose account grant the platform cannot confirm) counts as a failed probe: a warning, the host match, and nothing bound. The full verdict table, including a tree pulled while the identity probe was failing, is in [Teams, git, and CI](/cli/local-tree/workflow.md#the-account-binding).

## Concurrency and the tree lock

Requests run through a bounded pool — `--concurrency <n>` (1–16), else `CELIGO_CONCURRENCY`, else 8 — with the order kept where it matters: creates in dependency rounds, deletes dependents-first, and the report in plan order. A `429 Too Many Requests` is retried after the server's `Retry-After`. `pull` and `push` hold `.celigo/local/lock` for the run; a second run in the same directory fails fast, a lock whose process is gone or older than six hours is taken over with a note, and `--force-lock` is for a run that died on another machine sharing the directory. `push --dry-run` never locks.

Two pushes from two machines are not serialized by the lock — one writer per tree at a time is a team rule, and the deploy job's concurrency group in [Teams, git, and CI](/cli/local-tree/workflow.md) is how CI enforces it.

## Gotchas

* **`push --dry-run` is the review artifact.** Post it on the pull request together with `celigo diff --exit-code`; every guard runs at plan time, so what the dry run accepts is what the push sends.
* **Commit the manifest after every push.** A successful push refreshes the lock tokens in `.celigo/manifest.json`; a teammate pushing from a checkout without that commit is refused as `remote changed since pull`.
* **Do not pass `--force-remote` in CI.** Re-pull and re-commit the tree instead; the flag overwrites whatever someone changed in the UI.
* **`--no-create` in the deploy job** keeps a stray draft from becoming a resource; creates then go through a reviewed pull request that pushes from a developer machine, or through a job that allows them deliberately.
* **`lastModified` never appears in a file.** The lock token lives in the manifest; a diff of two tree files never shows it.
* **Some dry runs need a token.** `--dry-run --prune` reads each planned delete's server copy and what binds each planned topic delete, and `--dry-run --allow-connector-settings` reads the integration it saves.

## Related

* [Status and diff](/cli/local-tree/status-and-diff.md) — the offline and live views of what a push would send.
* [Lint and resolve](/cli/local-tree/lint-and-resolve.md) — the same guards and logic rules, offline, without a token.
* [Promote to another account](/cli/local-tree/promote.md) — the same plan-time guards, logic lint included, against another account.
* [Neutral trees](/cli/local-tree/neutral-trees.md) — how push resolves paths and `@bindings/` keys.
* [Teams, git, and CI](/cli/local-tree/workflow.md) — the deploy job, the manifest write-back commit, one writer per tree.
