# Workflow template reference

A workflow is one JSON file in your project, `workflows/<key>.workflow-v3.json`. The editor reads and writes it, and `dcupl workflow deploy` sends it to a runner. This page lists the fields of that file, the globals a script node runs with, how `{{…}}` templates in node settings resolve, how to call a deployed workflow, and the limits a run works within.

The `-v3` in the file name, the `TemplateV3` schema name and the `"version": "3"` field inside the file are the template format version. Versions 1 and 2 are legacy step-pipeline formats (`steps[]`); new workflows use the node-graph format, format version 3.

For the exact `config` fields of one node type, ask the CLI: `dcupl schemas get <Schema>` prints the schema the template is validated against (the schema names are in [Node types](#node-types)), and `dcupl schemas get TemplateV3 --example` prints a complete template.

## The template file

| Field         | Required | Value                                                                                                                         |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `key`         | yes      | The workflow key. Deploying prefixes it with your project ID, see [Calling a deployed workflow](#calling-a-deployed-workflow) |
| `config`      | yes      | The graph: `{ "version": "3", "nodes": [...], "edges": [...] }`                                                               |
| `name`        | no       | Display name, also used in reporting messages                                                                                 |
| `description` | no       | Shown in the console                                                                                                          |
| `version`     | no       | A version label of your own                                                                                                   |
| `variables`   | no       | Template variables, see [Variables](#variables)                                                                               |
| `ui`          | no       | Editor layout: `positions` (node id → `{ x, y }`) and `notes` (canvas notes). The runner ignores it                           |

## Nodes

Every entry of `config.nodes` has these fields:

| Field                 | Value                                                                                                                                                              |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                  | Unique within the template. Edges and `ui.positions` refer to it                                                                                                   |
| `name`, `description` | Shown in the editor                                                                                                                                                |
| `nodeClass`           | `trigger`, `step` or `response`                                                                                                                                    |
| `type`                | The node type, see below                                                                                                                                           |
| `inputs`              | Input ports, `{ "type": "main", "displayName"?, "required"? }`. Triggers have none. With `required: true` the node is skipped when no item arrives                 |
| `outputs`             | Output ports, `{ "type": "main", "displayName"? }`. Responses have none. An edge can only use a port the node declares                                             |
| `config`              | The settings of the node type                                                                                                                                      |
| `merge`               | Step nodes only: how the items of several incoming edges are combined, see [Merge Strategies](/console/workflows#merge-strategies). Needs at least two input ports |
| `skip`                | Step nodes only: `true` skips the node, so it runs nothing and sends nothing on                                                                                    |

### Node types

Trigger nodes (`nodeClass: "trigger"`):

| `type`            | Main `config` fields                                                |
| ----------------- | ------------------------------------------------------------------- |
| `trigger-request` | `path`, `methods` (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`), `auth` |
| `trigger-cron`    | `cronTime` (5 fields, or 6 with seconds), `timezone` (default UTC)  |

Step nodes (`nodeClass: "step"`):

| `type`           | Main `config` fields                                                                                                          |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `request`        | `url`, `method`, `headers`, `body`, `autoParse` (default `true`), `responseType`                                              |
| `script`         | `script`                                                                                                                      |
| `data-mapper`    | `mappings` (`{ origin, target }`), `model`                                                                                    |
| `dcupl-instance` | `script`, `processOptions`, `loaderFetchOptions`, `initOptions` (`config.apiKey`: a project API key uid, see below), `models` |
| `dcupl-files`    | `auth.apiKey`, `version`, `files` (`action`: `read`, `write`, `delete`, `move`, `copy`)                                       |
| `s3-files`       | `auth` (`accessKeyId`, `secretAccessKey`, `region`), `bucket`, `files`                                                        |
| `azure-files`    | `auth.connectionString`, `containerName`, `files`                                                                             |
| `git-files`      | `auth` (`user`, `token`), `repository` (`user`, `repo`, `branch`), `message`, `files`                                         |
| `dcupl-api`      | `operation`: `email`, `digest` or `executions`, see [The dcupl API node](/console/workflows/reporting#the-dcupl-api-node)     |

Response nodes (`nodeClass: "response"`):

| `type`            | Main `config` fields                                         |
| ----------------- | ------------------------------------------------------------ |
| `response-script` | `script`, `headers`, see [Script Response](#script-response) |
| `response-status` | none: `config` is `{}`                                       |

The schema names for `dcupl schemas get` are `TriggerRequestConfig`, `TriggerCronConfig`, `ResponseScriptConfig`, `ResponseStatusConfig`, and for the steps the type plus `StepConfig`: `RequestStepConfig`, `ScriptStepConfig`, `DataMapperStepConfig`, `DcuplInstanceStepConfig`, `DcuplFilesStepConfig`, `S3FilesStepConfig`, `AzureFilesStepConfig`, `GitFilesStepConfig`. The dcupl API node has no schema in the CLI.

The output ports the editor creates: `main` for triggers; `main`, `error`, `client-error`, `server-error` and `timeout` for a Request node; `main`, `error`, `not-found`, `permission-denied` and `already-exists` for the file nodes; `main`, `error` and `permission-denied` for a dcupl API node; `main` and `error` for the other steps, plus any ports you add to a Script node.

The code of a `script`, `dcupl-instance` or `response-script` node is the string in `config.script`, not `code`.

A trigger's `auth` is a list of `{ "type": "api-key", "value": "<api-key-uid>" }`. In the file, `value` is the uid of a project API key (Project Settings → API Keys); deploying resolves it to the key, which callers then send. Without `auth`, or with an empty list, the trigger is open.

The same resolution applies to a `dcupl-instance` node's `initOptions.config.apiKey` and to the file nodes' credentials (`dcupl-files` `auth.apiKey`, `git-files` `auth.token`, `azure-files` `auth.connectionString`, `s3-files` `auth.accessKeyId` and `auth.secretAccessKey`): a value that is the uid of a project API key is replaced with the key when you deploy, and any other value is kept as it is. A `dcupl-instance` node added in the editor gets the uid of the project's **Workflow Execution Key** in `initOptions.config.apiKey` when the project has one (the oldest projects have none, and the node then starts with no key); a private project, which every new project is by default, needs a key there for the node to load its loader. `{{…}}` placeholders are not filled in inside `initOptions`.

## Edges

| Field    | Value                                                                    |
| -------- | ------------------------------------------------------------------------ |
| `id`     | Unique within the template                                               |
| `source` | `{ "nodeId", "port" }`: an output port of the source node                |
| `target` | `{ "nodeId", "port" }`: an input port of the target node, usually `main` |

An edge also accepts `condition` and `transform`, but the runner ignores both, see [Edge Conditions and Transforms](/console/workflows#edge-conditions-and-transforms).

### Graph rules

`dcupl workflow validate` checks these locally, before anything is deployed:

- Node ids are unique, and edge ids are unique.
- There is at least one trigger. Triggers have no input ports and no incoming edges; response nodes have no output ports and no outgoing edges.
- Every edge names nodes that exist and ports those nodes declare.
- `merge` needs at least two input ports on its node, and `merge-by-key` needs a `key`.
- A Request node's `url` is a valid URL or contains a `{{…}}` template.

With `--runner <uid>`, the console API validates the template too: it checks for cycles, for the project's node and edge limits, and that no two request triggers handle the same path and method. A node without any edge gives a warning.

## Variables

A template variable is `{ "name", "value", "description" }`. `variables` can also be a flat map of string values, `{ "name": "value" }`, which is the form the console deploys and the runner reads. When you deploy, the runner gets one value per name, from (highest priority first):

1. Deployment overrides (**Deploy with variables…**)
2. Project variables (**Global Workflow Variables**)
3. The template's `variables`

Values are resolved at deploy time: after changing one, redeploy. Read them with `{{variables.name}}` in node settings and with `$context.variables.name` in scripts.

## Templates in node settings

The string settings of Request, file and dcupl API nodes (a URL, a header, a body, a file path, file content, a mail subject) can contain `{{…}}` placeholders. They are filled in once for each input item:

| Placeholder          | Value                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `{{$json.field}}`    | A field of the input item's JSON; a dotted path reaches nested fields (`{{$json.address.city}}`)                    |
| `{{$json}}`          | The whole input item's JSON, as a JSON string                                                                       |
| `{{variables.name}}` | A workflow variable. The name is read as is: `{{variables.a.b}}` looks up a variable called `a.b`                   |
| `{{request.body.x}}` | The trigger request: `request.body.…`, `request.query.…`, `request.headers.…`, `request.params.…`, `request.method` |
| `{{$state.key}}`     | A value an upstream script stored with `$state.set()`; `{{$state}}` is the whole store as JSON                      |

A value is inserted with `String()`, so an object becomes `[object Object]`; use `{{$json}}` to send a whole object. A placeholder that does not resolve raises `Unresolved template placeholder {{…}}`; it is never left in the text or replaced with an empty string. What the node does with that error depends on its type:

- **Request**, **dcupl Files** and **Git Files**: the whole node fails. One error item goes to its `error` port and the results of its other items are dropped; without an `error` edge the run fails.
- **S3 Files** and **Azure Files**: that input item yields an item `{ error, files: [], ok: false }` on `main`, and the other items go on.
- **dcupl API**: that input item yields an error item on the `error` port, and the other items go on. A field of the request body is `{{request.body.x}}`, not `{{request.x}}`.

## Script globals

A script node's code is the body of an async function that runs in an isolated JavaScript sandbox. It gets the node's input items and these globals:

| Global                                                                                 | What it gives you                                                                                                                                                   |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$json()`                                                                              | The JSON of every input item, as an array                                                                                                                           |
| `$json.first()`, `$json.last()`, `$json.at(i)`                                         | The JSON of one input item, or `null` when there is none                                                                                                            |
| `$json.fromPort(name)`                                                                 | The JSON of the items that arrived on input port `name`                                                                                                             |
| `$items()`, `$items.first()`, `.last()`, `.at(i)`, `.fromPort(name)`                   | The same as whole items, `{ json, pairedItem }`                                                                                                                     |
| `$input.ports()`, `$input.hasPort(name)`, `$input.count()`, `$input.countByPort(name)` | Which input ports received items, and how many                                                                                                                      |
| `$context`                                                                             | `{ executionId, workflowKey, nodeId, variables, request }`; `request` is the trigger request (`method`, `url`, `headers`, `query`, `params`, `body`)                |
| `$state`                                                                               | A store shared by the script nodes of one run: `get(key, fallback?)`, `set(key, value)`, `has`, `delete`, `clear`, `keys`, `values`, `entries`. Values must be JSON |
| `$ports(map)`, `_output.route(map)`                                                    | Send items to named output ports, see below                                                                                                                         |
| `console.log`, `.info`, `.warn`, `.error`, `.debug`                                    | Captured only when the run is traced (**Enable Data Trace** in a test run); otherwise they do nothing                                                               |

`$json` is a function: `$json.title` is `undefined`, `$json.first().title` is the field. The item a Request node emits holds the response in `body` (`$json.first().body`), next to `headers`, `statusCode`, `statusText`, `contentType` and `ok`. The item a request trigger emits holds `method`, `headers`, `query`, `params` and `body`.

The helpers are namespaced objects, not bare functions:

| Helper      | Functions                                                                                                                                                                                                        |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_csv`      | `toJSON`, `fromJSON`, `headers`, `validate`, `parseCsvLine`, `escapeCsvField`                                                                                                                                    |
| `_json`     | `toCSV`, `distinct`, `groupBy`, `flatten`, `unflatten`, `merge`, `pick`, `omit`, `parse`, `stringify`                                                                                                            |
| `_array`    | `chunk`, `unique`, `flatten`, `groupBy`, `sortBy`, `pluck`, `sum`, `avg`, `min`, `max`, `intersect`, `difference`, `sample`, `shuffle`                                                                           |
| `_string`   | `slugify`, `capitalize`, `camelCase`, `snakeCase`, `kebabCase`, `truncate`, `template`, `stripHtml`, `escapeHtml`, `unescapeHtml`, `toBase64`, `fromBase64`, `pad`, `reverse`, `count`, `startsWith`, `endsWith` |
| `_object`   | `get`, `set`, `has`, `merge`, `clone`, `pick`, `omit`, `mapKeys`, `mapValues`, `invert`, `keys`, `values`, `isEmpty`                                                                                             |
| `_datetime` | `now`, `toISO`, `format`, `parse`, `add`, `subtract`, `diff`, `isBefore`, `isAfter`, `isSame`, `startOf`, `endOf`                                                                                                |
| `_xml`      | `parse`, `toJSON`, `stringify`, `fromJSON`, `validate`                                                                                                                                                           |
| `_output`   | `route`, `create`, `partition`, `split`, `main`, `error`, `success`, `isValid`                                                                                                                                   |
| `_nodes`    | `dcuplFiles`, `gitFiles`, `s3Files`, `azureFiles`, `request`, each with `prepare(ops)`, which returns the operations as items for a downstream node                                                              |

`_http` and `_crypto` exist, but every call throws: fetch with a Request node, and keep secrets in workflow variables. `_json.toCSV`, `_string.toBase64` and `_string.fromBase64` also throw in a script node; build CSV with `_csv.fromJSON`. The Console's script editor completes the globals and helpers, and its snippets show an example for each.

The sandbox has the standard JavaScript built-ins (`JSON`, `Math`, `Date`, `Promise`, …) and nothing from Node.js or the browser: no `fetch`, no `require`, no `process`, no `Buffer`, no timers.

### What a script returns

| The script…                                       | Output                                                                           |
| ------------------------------------------------- | -------------------------------------------------------------------------------- |
| returns an array                                  | One item per entry on `main`                                                     |
| returns an object                                 | One item on `main`, the object is its JSON                                       |
| returns a primitive (`42`)                        | One item `{ "value": 42 }` on `main`                                             |
| returns nothing (`undefined`, `null`)             | The input items, unchanged, on `main`                                            |
| returns `$ports({ … })` or `_output.route({ … })` | Each key is a port: an array becomes one item per entry, a single value one item |
| throws                                            | The error goes to the node's `error` port                                        |

A plain object such as `return { valid: [...], invalid: [...] }` is data, not routing: it lands on `main` as one item. Items on an unconnected `main` port just end the branch. Items on any other port without an outgoing edge are replaced by one generated error item on the `error` port, and if `error` has no edge either, the run fails. To connect a port, declare it in the node's `outputs` (**+ Add Output** on the node's **Ports & Schema** tab).

```js title="Route items to two ports"
const products = $json.first().body.products || [];

return _output.route({
  valid: products.filter((p) => p.price > 0),
  invalid: products.filter((p) => !(p.price > 0)),
});
```

A script that runs longer than the script timeout fails with `Script execution timed out after 500ms` (see [Limits](#limits)).

## Script Response

A Script Response node runs its script with the same globals, plus four shortcuts: `data` (the same as `$json()`), `executionId`, `variables` and `request`. What the script returns is the HTTP response body:

- Return an **object**. Its fields are the response; an array is spread into an object with the keys `"0"`, `"1"`, …, so wrap it: `return { products: data };`.
- To set response headers, return `{ "data": …, "headers": { … } }`: `data` becomes the body and `headers` are added to the node's configured `headers`.

## Calling a deployed workflow

Deploying registers the workflow on the runner under `<projectId>-<key>`. A runner set up without the project prefix uses the bare `key`; the Workflows page shows the deployed key. A request trigger answers at:

```text
{runner-url}/v3/workflow/{deployed-key}/{path}
```

`{path}` is the trigger's `path` without its leading slash. A segment written `:name` in the trigger's path matches one path segment, which reaches the run as `request.params.name`. A request whose method or path matches no trigger gets `404` with the list of `availableTriggers`.

If the trigger has `auth`, send one of its keys in the `api-key` header or in the `api-key` (or `apiKey`) query parameter. A missing or wrong key, and a deployed key the runner does not know, are answered with `403`.

```bash
curl -X POST 'https://<runner-url>/v3/workflow/<project-id>-product-sync/products/sync' \
  -H 'api-key: <api-key>' \
  -H 'content-type: application/json' \
  -d '{ "limit": 5 }'
```

### The response

| The run ends in…       | Response body                                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| a Script Response node | What the script returns. A failed run answers `{ "error": { "message", "detail", "errorType", "nodeId" } }`                                                 |
| an HTTP Response node  | `{ "status": "completed", "executionId", "duration" }`. A failed run answers `"status": "failed"` and an `error` with `message` and `detail`                |
| no response node       | `{ "status", "executionId", "data", "metadata": { "duration", "nodesExecuted", "itemsProcessed" } }`; `data` holds the items of the nodes that end a branch |

A failed run is answered with `504` when it timed out, `507` when it ran out of memory, and `500` otherwise. A run that reaches its time limit is asked to stop and keeps its response node's failure shape: `{ "error": { "message", "detail", "errorType": "TimeoutError", "nodeId" } }` for a Script Response, `{ "status": "failed", "executionId", "duration", "error" }` for an HTTP Response. The runner kills a run that runs out of memory, or that has not stopped 3 s after its time limit: a killed run answers in the shape of a run without a response node (`"status": "failed"`, `"data": []`, an `error` and `metadata`), even when it has a response node. The response of a synchronous run carries the run's id in the `X-Dcupl-Execution-Id` header.

The response comes from the first node in the template's `nodes` list that ends a branch with items. In a graph with a response node and another branch that ends in a step node, let that step return `[]`, or its items may become the answer instead.

### Asynchronous runs

Add `?async=true` to answer at once instead of waiting for the run:

```json
{
  "jobId": "abc123xyz456",
  "status": "queued",
  "workflowKey": "<project-id>-product-sync",
  "statusUrl": "…/v3/workflow/<project-id>-product-sync/jobs/abc123xyz456/status",
  "eventsUrl": "…/v3/workflow/<project-id>-product-sync/jobs/abc123xyz456/events",
  "createdAt": "2026-10-13T09:00:00.000Z"
}
```

Poll `GET {runner-url}/v3/workflow/{deployed-key}/jobs/{jobId}/status` with the same key as the trigger. It answers `{ jobId, status, workflowKey, executionId, createdAt, startedAt, completedAt, result, error, metadata }`, where `status` is `queued`, `running`, `completed`, `failed` or `cancelled` and `result` holds the run's result once it is `completed`. `DELETE …/jobs/{jobId}` cancels a job, and `GET …/jobs/{jobId}/events` streams its progress as server-sent events. The runner keeps a finished job for one hour.

Past runs, synchronous or not, are listed in the editor's **History** and by `dcupl workflow history`, with the status `running`, `completed`, `failed` or `cancelled`.

## Limits

A run works within these limits. The time, memory, node, edge and deployed-workflow limits are project settings that dcupl can raise for a project; the others are fixed.

| Limit                                                          | Default                                             |
| -------------------------------------------------------------- | --------------------------------------------------- |
| One run, from trigger to response                              | 10 s                                                |
| One HTTP call of a Request node                                | 5 s                                                 |
| One script of a Script, Script Response or dcupl Instance node | 500 ms                                              |
| Memory of a run                                                | 128 MB                                              |
| Nodes per workflow                                             | 100                                                 |
| Edges per workflow                                             | 100                                                 |
| Items on one edge                                              | 1,000,000                                           |
| Deployed key (`<projectId>-<key>`)                             | 4–49 characters of `a-z`, `A-Z`, `0-9`, `_` and `-` |
| Deployed workflows per project                                 | 1                                                   |

The console's create form allows only lowercase letters, numbers and hyphens in the key. The console API refuses a key with `/`, a backslash, `?`, `#`, `%` or a control character when you deploy. Any other character outside the set in the table deploys, but the runner doesn't load the workflow, and calls to it get `403`. The Workflows page shows the count as **Deployed: n/max**. A workflow counts once per runner it is deployed to, and redeploying it to the same runner doesn't add one; past the limit, a deploy fails with `V3 workflow limit reached: <n>`.

## Accepted but not used

The schema accepts two edge fields that the runner does not act on. Leave them out:

- `condition` and `transform` on an edge (the editor lets you set them, see [Edge Conditions and Transforms](/console/workflows#edge-conditions-and-transforms))

The following fields are no longer part of the schema, because the runner never read them. A stored template that still has them is accepted and the fields are dropped:

- `config` on an edge (its only field was `bufferSize`): an edge holds up to 1,000,000 items, whatever it says
- `executionMode` and `batchSize` on a node: every node processes its whole input at once
- `merge.timeout`
- `config.catch`, a workflow-wide error node: route errors through each node's `error` port instead

## Example

The workflow of [Getting started](/console/workflows/getting-started) as a template file. It passes `dcupl workflow validate`:

```json filename="workflows/product-sync.workflow-v3.json"
{
  "key": "product-sync",
  "name": "Product Sync",
  "variables": [
    { "name": "productsApiUrl", "value": "https://dummyjson.com/products", "description": "Product API base URL" }
  ],
  "config": {
    "version": "3",
    "nodes": [
      {
        "id": "trigger",
        "name": "HTTP Trigger",
        "nodeClass": "trigger",
        "type": "trigger-request",
        "inputs": [],
        "outputs": [{ "type": "main" }],
        "config": { "path": "/products/sync", "methods": ["POST"] }
      },
      {
        "id": "respond",
        "name": "Return Products",
        "nodeClass": "response",
        "type": "response-script",
        "inputs": [{ "type": "main" }],
        "outputs": [],
        "config": { "script": "return { count: data.length, products: data };" }
      },
      {
        "id": "fetch",
        "name": "Fetch Products",
        "nodeClass": "step",
        "type": "request",
        "inputs": [{ "type": "main" }],
        "outputs": [
          { "type": "main" },
          { "type": "error" },
          { "type": "client-error" },
          { "type": "server-error" },
          { "type": "timeout" }
        ],
        "config": {
          "method": "GET",
          "url": "{{variables.productsApiUrl}}?limit={{request.body.limit}}&select=title,price,stock"
        }
      },
      {
        "id": "validate",
        "name": "Validate Products",
        "nodeClass": "step",
        "type": "script",
        "inputs": [{ "type": "main" }],
        "outputs": [{ "type": "main" }, { "type": "error" }, { "type": "valid" }, { "type": "invalid" }],
        "config": {
          "script": "const products = $json.first().body.products || [];\nconst isValid = (p) => Boolean(p.title) && p.price > 0 && p.stock >= 50;\nreturn _output.route({\n  valid: products.filter(isValid),\n  invalid: products.filter((p) => !isValid(p)),\n});"
        }
      },
      {
        "id": "transform",
        "name": "Transform Products",
        "nodeClass": "step",
        "type": "script",
        "inputs": [{ "type": "main" }],
        "outputs": [{ "type": "main" }, { "type": "error" }],
        "config": {
          "script": "return $json().map((p) => ({ id: p.id, name: p.title, price: p.price, stock: p.stock }));"
        }
      },
      {
        "id": "log",
        "name": "Log Errors",
        "nodeClass": "step",
        "type": "script",
        "inputs": [{ "type": "main" }],
        "outputs": [{ "type": "main" }, { "type": "error" }],
        "config": {
          "script": "$json().forEach((p) => console.log('low stock', p.id, p.title, p.stock));\nreturn [];"
        }
      }
    ],
    "edges": [
      {
        "id": "e1",
        "source": { "nodeId": "trigger", "port": "main" },
        "target": { "nodeId": "fetch", "port": "main" }
      },
      {
        "id": "e2",
        "source": { "nodeId": "fetch", "port": "main" },
        "target": { "nodeId": "validate", "port": "main" }
      },
      {
        "id": "e3",
        "source": { "nodeId": "validate", "port": "valid" },
        "target": { "nodeId": "transform", "port": "main" }
      },
      {
        "id": "e4",
        "source": { "nodeId": "validate", "port": "invalid" },
        "target": { "nodeId": "log", "port": "main" }
      },
      {
        "id": "e5",
        "source": { "nodeId": "transform", "port": "main" },
        "target": { "nodeId": "respond", "port": "main" }
      }
    ]
  }
}
```

## Next steps

- [Workflows](/console/workflows) — nodes, ports, merge strategies and error handling in the editor
- [Getting started](/console/workflows/getting-started) — build this workflow in the Console
- [CLI workflows](/cli/workflows) — validate, test and deploy a template file
- [Schemas](/cli/schemas) — `dcupl schemas get` for every node's config
