Skip to content

feat(api): add workflow export and import endpoints to the public v1 API - #5999

Merged
waleedlatif1 merged 4 commits into
stagingfrom
worktree-workflow-import-api
Jul 28, 2026
Merged

feat(api): add workflow export and import endpoints to the public v1 API#5999
waleedlatif1 merged 4 commits into
stagingfrom
worktree-workflow-import-api

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Add GET /api/v1/workflows/{id}/export — returns a portable JSON envelope (version, exportedAt, workflow, state) matching the admin export's shape
  • Add POST /api/v1/workflows/import — accepts that envelope verbatim, a bare workflow state, or a JSON string of either, so workflows round-trip across workspaces over the public API
  • The admin namespace already had both halves; the public v1 surface had neither
  • Public export is secret-sanitized (sanitizeForExport): stored credentials and password: true fields are redacted, {{ENV_VAR}} references and block positions preserved. Admin export stays raw for backup/restore
  • Import regenerates block/edge/loop/parallel ids and de-duplicates the name against the target folder, so the same payload can be imported repeatedly and alongside its source
  • Export requires read, import requires write on the workspace. Export masks permission failures as 404 to match the rest of the v1 workflow read surface. Import authorizes before parsing any payload
  • Import body capped at 10 MB (well under the platform-wide 50 MB default)
  • Export records a workflow.exported audit entry; import inherits workflow.created from the shared orchestrator
  • Document both in apps/docs/openapi.json + the workflows nav, with new WorkflowExport, WorkflowExportState and ImportedWorkflow schemas
  • Move parseWorkflowVariables from app/api/v1/admin/types.ts to lib/workflows/variables/parse.ts so a public route doesn't import from the admin namespace (4 admin consumers repointed)

Type of Change

  • New feature

Testing

26 new tests (8 export, 18 import); 50 pass across app/api/v1/workflows/. Covers auth, permission masking, secret redaction, env-var preservation, edge-handle normalization, all three accepted payload shapes, metadata precedence, legacy array-form variables, and rollback of the created row when persisting state fails.

bunx tsc --noEmit, bun run lint, bun run check:api-validation:strict and the monorepo boundary check all pass. Not exercised against a live server.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

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.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 28, 2026 3:27am

Request Review

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Introduces a workspace write path that persists full workflow graphs and handles secret redaction on export; behavior is heavily tested and aligned with existing editor/admin flows, but mistakes could leak credentials or leave broken workflows.

Overview
Adds public v1 workflow portability: GET /api/v1/workflows/{id}/export returns a versioned JSON envelope (with sanitizeForExport redacting stored credentials and workspace-scoped bindings while keeping {{ENV_VAR}} refs), and POST /api/v1/workflows/import creates a new workflow from that envelope, bare state, or a JSON string—with write auth, 10 MB cap, id regeneration, name dedup, transactional graph + variables persistence, and rollback on failure.

Refactors shared import/export plumbing: prepareWorkflowStateForPersistence is extracted so editor PUT /api/workflows/[id]/state, v1 import, and admin import normalize graphs the same way; parseWorkflowVariables / normalizeImportedVariables move to lib/workflows/variables/parse; admin import stops writing raw parsed state. OpenAPI, docs nav, v1 contracts/middleware, and route tests (plus round-trip / prepare-state coverage) are updated; import id regeneration also escapes regex metacharacters in block-name reference rewrites to avoid ReDoS on hostile payloads.

Reviewed by Cursor Bugbot for commit a6d6213. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds public v1 workflow export/import APIs with sanitization, atomic import persistence, docs, and shared variable parsing.

  • GET /api/v1/workflows/{id}/export returns a portable envelope via sanitizeForExport (block secrets/workspace bindings redacted; variables and {{ENV_VAR}} kept by design)
  • POST /api/v1/workflows/import accepts export envelope, bare state, or JSON string; regenerates ids; write-gated; graph + variables in one transaction with shell-row rollback
  • Docs/OpenAPI, contracts, admin import aligned on prepareWorkflowStateForPersistence / normalizeImportedVariables; parseWorkflowVariables moved out of admin types

Confidence Score: 5/5

Safe to merge; prior export-docs and import atomicity threads are addressed and no remaining blocking failures were identified.

Import now writes graph and variables in a single transaction and deletes the shell row on any failure, with regression coverage; export documents intentional variable pass-through aligned with existing read APIs. No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/app/api/v1/workflows/import/route.ts Import authorizes write, validates state, then persists graph and variables in one transaction with compensating delete on failure (addresses prior half-land issue).
apps/sim/app/api/v1/workflows/[id]/export/route.ts Public export sanitizes block secrets/bindings and documents that variables remain plaintext configuration matching GET workflow.
apps/sim/lib/workflows/variables/parse.ts Shared parse/normalize helpers for export and import variable shapes (including legacy arrays).
apps/sim/lib/workflows/persistence/prepare-state.ts Shared persistence preparation used by public and admin import paths.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Export as GET /v1/workflows/{id}/export
  participant Import as POST /v1/workflows/import
  participant DB as Postgres

  Client->>Export: API key + workflow id
  Export->>DB: load workflow + normalized state
  Export-->>Client: envelope (sanitized blocks, variables as stored)

  Client->>Import: workspaceId + envelope
  Import->>DB: create shell workflow
  Import->>DB: transaction save graph + variables
  alt persist fails
    Import->>DB: delete shell workflow
    Import-->>Client: 500
  else success
    Import-->>Client: 201 ImportedWorkflow
  end
Loading

Reviews (4): Last reviewed commit: "fix(api): cap import names inside the bo..." | Re-trigger Greptile

Comment thread apps/sim/app/api/v1/workflows/[id]/export/route.ts
Comment thread apps/sim/app/api/v1/workflows/import/route.ts
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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5decc01. Configure here.

…eline

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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/v1/workflows/import/route.ts
…t 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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a6d6213. Configure here.

@waleedlatif1
waleedlatif1 merged commit ca13cc9 into staging Jul 28, 2026
20 checks passed
@waleedlatif1
waleedlatif1 deleted the worktree-workflow-import-api branch July 28, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant