Skip to content

Commit ca13cc9

Browse files
authored
feat(api): add workflow export and import endpoints to the public v1 API (#5999)
* feat(api): add workflow export and import endpoints to the public v1 API Adds GET /api/v1/workflows/[id]/export and POST /api/v1/workflows/import. The export envelope is accepted verbatim by import, so workflows round-trip between workspaces over the public API. Unlike the admin export, the public export is secret-sanitized: stored credentials and password fields are redacted while {{ENV_VAR}} references and block positions are preserved. Import regenerates block, edge, loop and parallel ids and de-duplicates the workflow name against the target folder. Also moves parseWorkflowVariables out of the admin types module into lib/workflows/variables/parse.ts so the public route does not import from the admin namespace. * fix(api): make workflow import atomic and clarify what export redacts Writes the imported graph and its variables in a single transaction and deletes the shell workflow row on any failure, so a caller that receives an error is never left with a partially imported workflow. Previously a throw from the variables update returned 500 while leaving the workflow behind with an empty variables map. Also narrows the export route's sanitization claim: workflow variables are emitted as stored, matching GET /api/v1/workflows/[id] and the in-app export. They are plaintext configuration readable at the same permission level this route requires; secrets belong in environment variables, which travel as unresolved references. * fix(api): close import defects found in audit and share one write pipeline Security: - Escape block names before interpolating them into a RegExp in updateValueReferences. Names reach it straight from imported workflow JSON and normalizeWorkflowBlockName preserves regex metacharacters, so a name like `a*a*a*a*b` compiled to a catastrophically backtracking pattern. A sub-kilobyte body blocked the event loop for 50s and grew exponentially. Also skip rename-to-itself, which is the entire map on the import path, so the scan no longer runs at all there. - Validate folder ownership before folder lock state, so a locked folder in another workspace can no longer be distinguished from a missing one. Correctness: - Gate the imported graph on workflowStateSchema, the same schema the canonical PUT /api/workflows/[id]/state path enforces. Without it a valid 201 could persist a block field of the wrong type, which then threw on every subsequent read and left a workflow nothing could open. - Guard the compensating delete so a failed rollback logs the orphaned id instead of vanishing into a generic 500. - Validate variable `type` against the enum and build the record on a null-prototype object, so a `__proto__` key no longer silently drops the variable. - Bound payload-derived names and descriptions to the same limits the contract declares for the explicit overrides. - Return the description as stored rather than coercing '' to null, matching GET /api/v1/workflows/[id]. Shared code, so the two write paths cannot drift: - Extract prepareWorkflowStateForPersistence and use it from both PUT /api/workflows/[id]/state and the v1 import route: agent-tool sanitization, block backfill, dangling-edge removal, and loop/parallel recomputation now have one implementation. - Persist inline custom tools on import, which the canonical path already did. - Move variable normalization into lib/workflows/variables and repoint the admin importer at it, removing the last duplicate. Docs: - OpenAPI: oneOf -> anyOf on the import body. WorkflowExport matches any object, so every valid object payload matched two branches and failed validation under any spec-driven validator. Document 423 and the loss of workspace-scoped bindings on export. Tests: prepare-state unit tests and a real export -> import round trip with no mocks of the sanitizer or parser, covering loop/parallel children and the regex-metacharacter payload. * fix(api): cap import names inside the bound and align the three import paths - `truncate` appends its suffix after slicing, so capping at the contract limit produced 203/2003-character values — past the very bound the cap exists to enforce, and into the headroom reserved for dedup suffixes. Reserve the ellipsis inside the limit. - Match `extractWorkflowName`'s candidate order (state.metadata.name before workflow.name) and trim, so the v1 API and the in-app importer resolve the same name for the same payload. Previously a hand-authored payload carrying both could yield two different names. - Run the admin importer through prepareWorkflowStateForPersistence too. It was writing raw parsed state, so a dangling edge tripped the workflow_edges foreign key and a block missing its backfilled columns could land unopenable — the same class this PR just closed on the v1 path.
1 parent 8b199d7 commit ca13cc9

21 files changed

Lines changed: 2310 additions & 161 deletions

File tree

apps/docs/content/docs/en/api-reference/(generated)/workflows/meta.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"cancelExecution",
66
"listWorkflows",
77
"getWorkflow",
8+
"exportWorkflow",
9+
"importWorkflow",
810
"deployWorkflow",
911
"undeployWorkflow",
1012
"rollbackWorkflow",

0 commit comments

Comments
 (0)