Apply Workflow Operations

Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.

Best-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in skipped, each with a machine-readable type; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. deferred is not a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.

Set atomic to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers 409 with error.details.code: "OPERATIONS_NOT_APPLIED", the same skipped array, and a droppedInputs array, having persisted nothing.

A block_id you supply on an add or insert_into_subflow is only a label unless it is already a UUID: the engine mints one and returns the pairing in mintedBlockIds. References between operations in the same batch are remapped for you, so triage can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.

Operation params is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: inputs keyed by sub-block id, with retry, triggerMode and advancedMode beside it rather than inside it, and connections keyed by source handle. GET /blocks/{blockId} publishes the inputs a given block type accepts. The Agent block’s inputs.tools value is the important exception to that open catalog shape: it is published here as the named AgentToolInput union, covering catalog integrations, workspace custom tools, and MCP tools.

lint is advisory and never blocks the write. lint.fieldIssues is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and lint.unresolvedReferences names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only inputValidationErrors lists inputs that were actually dropped.

As with PUT /workflows/{workflowId}/state, this changes only the draft; deploy to publish it. A workspace API key is rejected with 403; use a personal API key.

Set ?dryRun=true to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and lint findings the committed write would, with dryRun: true — but needsRedeployment describes the state before the write, and warnings raised by persistence itself are necessarily absent.

POST/api/v2/workflows/{workflowId}/operations
X-API-Key<token>

Your Sim API key, personal or workspace-scoped. Generate one under Settings, then API Keys. Operations that reject workspace keys say so in their own description.

In: header

Path Parameters

workflowId*string

Unique workflow identifier.

Length1 <= length

Query Parameters

dryRun?boolean

Validate and lint without persisting. The response is identical to the committed write of the same body, so a caller can inspect lint and then re-send the request for real. Nothing is written, no audit entry is recorded, and collaborators are not notified.

Request Body

application/json

A batch of semantic edits against a workflow graph.

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://www.sim.ai/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/operations" \  -H "X-API-Key: YOUR_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "operations": [      {        "operation_type": "add",        "block_id": "agent-1",        "params": {          "type": "agent",          "name": "Triage",          "inputs": {            "tools": [              {                "type": "cloudwatch",                "operation": "describe_alarm_history",                "usageControl": "auto",                "params": {}              }            ]          }        }      }    ]  }'
{
  "data": {
    "id": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
    "applied": 1,
    "skipped": [],
    "deferred": [],
    "inputValidationErrors": [],
    "mintedBlockIds": {
      "triage": "a3f1c0b2-7a44-4c1d-9d3a-2b8e5f0a1c77"
    },
    "lint": {
      "sources": [],
      "sinks": [],
      "orphanBlocks": [],
      "emptyOutgoingPorts": [],
      "invalidBranchPorts": [],
      "invalidConnectionTargets": [],
      "fieldIssues": [
        {
          "blockId": "agent-1",
          "blockName": "Triage",
          "blockType": "agent",
          "missingRequiredFields": [
            "systemPrompt"
          ],
          "inactiveModeValues": []
        }
      ],
      "unresolvedReferences": [],
      "notes": []
    },
    "warnings": [],
    "needsRedeployment": true,
    "dryRun": false
  }
}
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Invalid request"
  }
}
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API key required"
  }
}
{
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient workspace permissions",
    "details": {
      "code": "INSUFFICIENT_WORKSPACE_ROLE"
    }
  }
}
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Not found"
  }
}
{
  "error": {
    "code": "CONFLICT",
    "message": "Webhook path already in use"
  }
}
{
  "error": {
    "code": "PAYLOAD_TOO_LARGE",
    "message": "Request body is too large"
  }
}
{
  "error": {
    "code": "UNSUPPORTED_MEDIA_TYPE",
    "message": "Request body must be sent as application/json"
  }
}
{
  "error": {
    "code": "LOCKED",
    "message": "Workflow is locked"
  }
}
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "API rate limit exceeded",
    "details": {
      "retryAfter": "2026-01-01T00:00:30.000Z"
    }
  }
}
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  }
}
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Service temporarily unavailable"
  }
}