# Save a workflow as a template

**POST** `/api/templates`

Saves a copy of a workflow’s draft as a template.

Needs [an API token](https://docs.gorunner.io/api/authentication#scopes) with the **Write** permission for workflows (`workflow:write`).

## Parameters

### Body

- `workflowId` · Required · UUID: The workflow to copy, in this workspace. Its draft is copied as it is now.
- `name` · Optional · string: Up to 255 characters, trimmed. Blank: the workflow’s name.
- `description` · Optional · string: Trimmed. Blank stays blank: the template then describes itself by its trigger and first steps.
- `category` · Optional · string: Any label, trimmed.

## What is copied

The steps and their settings, plain variables and notes. Not sensitive variables, which a template can’t hold, and not a secret typed into a step: those are listed in `blanked`. [Preview saving](https://docs.gorunner.io/api/templates/preview-template-save) says which first.

Connections travel by ID and are matched when the template is used. The copy never changes when the workflow does; [update its steps](https://docs.gorunner.io/api/templates/replace-template-steps) to copy again.

In an organization it belongs to the organization; in a customer account, to this workspace. It takes no `Idempotency-Key`: a retry saves a second one.

## Responses

- 201 Created

  The template, with its copy of the steps and `blanked`: each value the copy left out.

- 400 `VALIDATION_ERROR`

  The body isn’t JSON, `workflowId` is missing or not a UUID, or the name is over 255 characters.

- 403 `FORBIDDEN`

  The token lacks the **Write** permission for workflows.

- 404 `NOT_FOUND`

  No workflow with this ID in the workspace.

- 500 `INTERNAL_ERROR`

  The template wasn’t saved. Try again.

Any call can also answer: [`401 UNAUTHORIZED`](https://docs.gorunner.io/errors#UNAUTHORIZED) · [`401 TOKEN_ENDED`](https://docs.gorunner.io/errors#TOKEN_ENDED) · [`403 TOKEN_SURFACE_OFF`](https://docs.gorunner.io/errors#TOKEN_SURFACE_OFF) · [`403 FEATURE_OFF`](https://docs.gorunner.io/errors#FEATURE_OFF) · [`403 EMBED_NOT_ALLOWED`](https://docs.gorunner.io/errors#EMBED_NOT_ALLOWED) · [`403 EMBED_NOT_AVAILABLE`](https://docs.gorunner.io/errors#EMBED_NOT_AVAILABLE) · [`403 ACCOUNT_SUSPENDED`](https://docs.gorunner.io/errors#ACCOUNT_SUSPENDED) · [`413 PAYLOAD_TOO_LARGE`](https://docs.gorunner.io/errors#PAYLOAD_TOO_LARGE) · [`415 INVALID_CONTENT_TYPE`](https://docs.gorunner.io/errors#INVALID_CONTENT_TYPE) · [`429 RATE_LIMITED`](https://docs.gorunner.io/errors#RATE_LIMITED) · [`503 BUSY`](https://docs.gorunner.io/errors#BUSY) · [`503 PAUSED`](https://docs.gorunner.io/errors#PAUSED) · [`503 SERVICE_UNAVAILABLE`](https://docs.gorunner.io/errors#SERVICE_UNAVAILABLE). See [Errors](https://docs.gorunner.io/api/errors#by-status).

Request

**curl**

```bash
GORUNNER="https://api.gorunner.io"
GORUNNER_TOKEN="grt_…"  # needs Write for workflows

curl -X POST "$GORUNNER/api/templates" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workflowId": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
    "name": "Order intake",
    "category": "Sales"
  }'
```

**JavaScript**

```js
const GORUNNER = "https://api.gorunner.io";
const TOKEN = process.env.GORUNNER_TOKEN;

const res = await fetch(`${GORUNNER}/api/templates`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    workflowId: "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
    name: "Order intake",
    category: "Sales",
  }),
});
const { data } = await res.json();
```

**Python**

```python
import os, requests

GORUNNER = "https://api.gorunner.io"
TOKEN = os.environ["GORUNNER_TOKEN"]

res = requests.post(
    f"{GORUNNER}/api/templates",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
      "workflowId": "3b1e5c2a-7d4f-4e8b-a1c6-5f2d9b0e7a31",
      "name": "Order intake",
      "category": "Sales",
    },
)
data = res.json()["data"]
```

Response · 201 Created, shortened

```json
{
  "data": {
    "id": "9c4e2a17-3d5b-4f86-b0e2-8a1c7d3f5e29",
    "scope": "org",
    "name": "Order intake",
    "description": "Webhook to fulfilment, with a VIP branch.",
    "category": "Sales",
    "triggerType": "webhook",
    "createdAt": "2026-10-20T08:00:00Z",
    "definition": {
      "schemaVersion": 2,
      "trigger": {
        "name": "trigger",
        "displayName": "Order received",
        "type": "webhook",
        "pieceId": "webhook",
        "pieceVersion": "1.0.0",
        "actionId": "catch_webhook",
        "config": {
          "methods": [
            "POST"
          ]
        },
        "valid": true,
        "nextAction": {
          "name": "forward",
          "displayName": "Forward to fulfilment",
          "type": "PIECE",
          "pieceId": "http",
          "pieceVersion": "1.0.0",
          "actionId": "send_request",
          "config": {
            "method": "POST",
            "url": "https://example.com/fulfilment",
            "body_type": "json",
            "body_json": "{{trigger.body}}"
          },
          "valid": true
        }
      },
      "settings": {}
    },
    "blanked": [
      {
        "step": "forward",
        "stepLabel": "Forward to fulfilment",
        "field": "auth_token",
        "fieldLabel": "Bearer Token"
      }
    ]
  }
}
```

**Every field**

### Body

- `workflowId` · Required · UUID: The workflow whose CURRENT DRAFT definition is snapshotted. Must be in the caller’s workspace. You never submit a definition yourself.
- `name` · Optional · string: Optional; trimmed. Blank falls back to the source workflow’s name. Max 255 characters.
- `description` · Optional · string: Optional; trimmed. Blank stays blank (the template then shows a generated `whenLine`).
- `category` · Optional · string: Optional free-text grouping label; trimmed. No server-side vocabulary.

### Response · 201 Created

- `data` · object: A template with its definition: an immutable snapshot of a workflow’s draft taken when it was saved (or its steps last updated). A copy, never a live link: workflows made from it are not affected by changing or deleting it.
  - `id` · UUID
  - `scope` · string: Who sees it: `org` — an organization’s, every workspace of it; `workspace` — a customer account’s, the workspace it was saved in only; `installation` — shared by the installation with its customer accounts (listed only in a customer account, and only while the license has embedding), read-only here. One of `org`, `workspace`, `installation`.
  - `key` · string: An installation template’s stable handle. Absent on every other template.
  - `name` · string
  - `description` · string: What it does, as saved. May be empty: `whenLine` then describes it.
  - `category` · string: Free-text grouping label; empty when none.
  - `triggerType` · string
  - `createdAt` · time
  - `updatedAt` · time
  - `publishedAt` · time: When an installation template was first offered. Absent on every other template.
  - `readable` · boolean: false: its definition cannot be read. Such a row is listed only to someone who may delete it (`canManage`), with everything derived from the definition empty.
  - `author` · object or null: Who saved it; null for an installation template.
    - `id` · UUID or null
    - `name` · string or null: Their name, else their email; null once the account is deleted.
    - `left` · boolean: They are no longer a member of this organization.
  - `source` · object or null: An installation template’s origin; null otherwise.
    - `organization` · string: The organization it was shared from; empty once that organization is gone.
  - `savedHere` · boolean: It was saved in this workspace. Otherwise its bound values arrive blank (see `TemplateFillField`).
  - `savedIn` · string or null: The workspace it was saved in (an installation template: the organization it was shared from); null when not recorded.
  - `sourceWorkflow` · object or null: The workflow it was saved (or last updated) from, while that workflow exists and is in this workspace.
    - `id` · UUID
    - `name` · string
  - `trigger` · object or null
    - `name` · string
    - `displayName` · string
    - `pieceId` · string
    - `actionId` · string
  - `marks` · array of strings: App ids in run order: the trigger’s, then one per step (branch steps `branch`, loops `loop`, a Code step `code`), never de-duplicated.
  - `steps` · array of objects: Every step, in the order of `marks` after the trigger.
    - `name` · string
    - `displayName` · string
    - `pieceId` · string
    - `actionId` · string
    - `type` · string: One of `PIECE`, `CODE`, `BRANCH`, `LOOP`.
    - `depth` · integer: 0 at the top level; one more inside a branch arm, a loop body or an error path.
  - `whenLine` · string: A generated line for a template without a description: how it starts, then its first three steps’ names.
  - `slots` · array of objects: The connections a workflow made from it needs, one per service account.
    - `key` · string
    - `pieceId` · string: The connection’s app (what `GET /api/connections` rows carry).
    - `pieceName` · string
    - `steps` · array of strings
    - `stepLabels` · array of strings
    - `connectionId` · string or null: The connection the template’s own steps name; null for an installation template or none.
  - `toFill` · integer: How many values using it here will ask for: required fields left blank and bound values blanked here.
  - `available` · boolean
  - `unavailable` · object or null: Why it cannot be used on this installation.
    - `code` · string: One of `PIECE_UNAVAILABLE`, `STEP_INCOMPLETE`, `ACTION_UNAVAILABLE`, `UNREADABLE`.
    - `message` · string
    - `step` · string: The step it is about, by name.
  - `uses` · object or null: Workflows of this organization made from it, and how many are live. Sent only to its author, an organization admin or a platform admin.
    - `workflows` · integer
    - `live` · integer
  - `canManage` · boolean: The caller may rename, update the steps of and delete it: its creator, an organization admin, or for a `workspace` template that workspace’s admin.
  - `createdBy` · UUID: Who saved it, as before the Templates redesign (`author` names them). Absent on an installation template and when the account is gone.
  - `definition` · object: A workflow definition — the same document the workflow API uses (`schemaVersion` is 2). It is a TREE, not a list: `trigger` is the root and each node chains onward through `nextAction`; `branches[].firstAction` and `firstLoopAction` hold nested sub-chains, and `onErrorAction` holds a step’s error arm. A step’s settings live in `config`, never `settings`. The trigger names its trigger in a field called `actionId`, and trigger `type` is lowercase while action `type` is UPPERCASE (`PIECE`, `BRANCH`, `LOOP`, `CODE`). A step that uses credentials carries `connectionId` — a bare connection id, workspace-scoped, which is why it may be cleared when the template is used in another workspace. A workflow definition: its fields are in [Workflow definition](https://docs.gorunner.io/api/workflow-definition).
  - `blanked` · array of objects: Only on a save’s and an update of the steps’ answers: each value the copy left out (a credential typed into a step).
    - `step` · string: The step’s name (the trigger’s own name for the trigger).
    - `stepLabel` · string: The step as the canvas names it.
    - `field` · string: The top-level input key.
    - `fieldLabel` · string: The app’s title for the input.

Related: [Preview saving a template](https://docs.gorunner.io/api/templates/preview-template-save) · [Make a workflow from a template](https://docs.gorunner.io/api/templates/use-template) · [Use and save templates](https://docs.gorunner.io/build/templates)

Source: https://docs.gorunner.io/api/templates/create-template
