feat: v1.7.0 — workflow archive / unarchive (reversible alternative to delete) - #3
Merged
Merged
Conversation
…o delete
n8nctl could only delete workflows, permanently. n8n has had a reversible
archive since PR #27513 (n8n-io/n8n, merged 2026-03-27) — this wires it up:
n8nctl workflow archive <id> # reversible; forces the workflow inactive
n8nctl workflow unarchive <id> # restores it, still inactive
Both follow the activate.ts shape and support --dry-run. Like activate and
delete they print a status line rather than a JSON document, so --json does
not change their output; wiring --json through the mutation verbs is a
separate change and is deliberately not smuggled in here.
## The version gap this has to handle
The instance this was developed against (n8n 1.122.5) returns `isArchived` on
the workflow model but 404s on POST /workflows/{id}/archive — the field
shipped BEFORE the endpoints did. So a 404 here is ambiguous: missing
workflow, or missing endpoint?
`archiveWithVersionGuard` resolves that on the error path only (no extra
request on the happy path): re-GET the workflow, and only a CONFIRMING 404
proves it is gone. A GET 200 means the endpoint is what is absent, so the
user gets an upgrade hint instead of a bare "verify the ID". Any other probe
failure (401 mid-command, 5xx, retries exhausted) is surfaced as itself —
reporting "workflow not found" for an auth or connectivity fault would blame
the user for the wrong thing.
`unarchive --dry-run` reuses the workflow it already fetched to flag the
documented 400 when the target is not archived, rather than reporting success
for a call that cannot succeed.
## Verified, not assumed
- Live: archive/unarchive 404 on n8npc 1.122.5 while GET returns 200 (raw
HTTP, confirming the guard's GET-200 branch actually fires); DELETE still
hard-deletes there; dry-run mutates nothing. Scratch workflow cleaned up.
- Source: `WorkflowService.archive` sets active=false and archiving an
already-archived workflow is a 200 no-op (skipArchived), while unarchiving a
non-archived one throws BadRequestError → 400. The messages state what the
code does, not what seemed likely.
- 12 new tests (502 total), coverage gate green, audit clean.
Endpoint contracts + the version gap recorded in SESSION_REST_CONTRACT.md.
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the gap the user hit: n8nctl could only
deleteworkflows, permanently.Maps to the n8n public API
POST /workflows/{id}/archive//unarchive(n8n-io/n8n#27513, merged 2026-03-27).
Both follow the existing
activate.tsshape and support--dry-run.The version gap this has to handle
The instance this was developed against (n8n 1.122.5) returns
isArchivedon theworkflow model but 404s on
POST /workflows/{id}/archive— the field shipped beforethe endpoints did. So a 404 here is ambiguous: missing workflow, or missing endpoint?
archiveWithVersionGuardresolves it on the error path only (no extra request on thehappy path):
GET /workflows/{id}That last row came out of review: a bare
catchwould have reported "workflow not found —verify the ID" for what is really an auth or connectivity fault, blaming the user for the
wrong thing.
unarchive --dry-runreuses the workflow it already fetched to flag the documented 400 whenthe target is not archived, rather than reporting success for a call that cannot succeed.
Verified, not assumed
GETreturns 200 (checked atraw HTTP, confirming the guard's GET-200 branch actually fires);
DELETEstill hard-deletesthere;
--dry-runmutates nothing. Scratch workflow created and cleaned up — no productionworkflow was archived.
WorkflowService.archivesetsactive=false; archiving analready-archived workflow is a 200 no-op (
skipArchived); unarchiving a non-archived onethrows
BadRequestError→ 400. The user-facing messages state what the code does.npm audit --omit=devclean, docsdrift gate clean.
Deliberately out of scope
--jsondoes not change these commands' output — they print a status line, exactly likeactivate/deactivate/delete. Review suggested wiringprintDatain; I left it alonebecause doing it for
archiveonly would make it the odd one out among the mutation verbs.Wiring
--jsonthrough all of them is a worthwhile separate change.Endpoint contracts + the version gap are recorded in
scripts/SESSION_REST_CONTRACT.md.🤖 Generated with Claude Code