Skip to content

Run Manifest ​

A Manifest is a JSON document that records what one attempt of a workflow run did: its state, every node's status, row counts and timing, route branch counts, reject totals, the files written, and the parameters the run was started with. Put a Manifest node on the canvas, point it at a file destination, and connect nodes into it. The Manifest covers the nodes you connect and everything upstream of them. When the run ends, Precept writes one document per Manifest node.

The document holds counts, names, types, timings, paths and client-safe error text. It never holds source data. Row values go only in the reject files.

Where it is written ​

The Manifest node's path is a template. Its tokens are:

TokenValue
{{runId}}The run's ID.
{{attempt}}The attempt number, starting at 1.
{{timestamp}}The run's admission time. It is the same for every attempt of a run.

The default is manifests/{{runId}}/attempt-{{attempt}}.json. An unknown token is an error.

Document fields ​

The schema is manifest.json (version 1.0).

FieldMeaning
$schemaThe schema URL.
schemaVersion"1.0".
manifestThe Manifest node's nodeId and title.
runid, attempt, workflowId (null when unsaved), startedBy (null when unknown), trigger (rerun for an attempt after the first, else watcher for a run a file watcher started, else api; ui is reserved and not sent in 1.0), engine, admittedAt (attempt 1's start, the same for every attempt), startedAt and endedAt (this attempt), state (completed, failed or cancelled) and error (client-safe text or null).
attemptsEvery attempt of the run, including this one: attempt, state, startedAt, endedAt.
restartsWorker restarts inside this attempt: generation, at, reason.
parametersThe parameters the run was started with.
scopenodeIds: the nodes the Manifest covers.
totalsnodes, nodesNotRun, rowsWritten and rejects, summed over the nodes in scope.
nodesOne entry per node in scope, in execution order. See below.
stagesThe stages that contain at least one node in scope: index, status, nodeIds, ms, peakBytes, spillBytes, blocksDropped and error.
leftoversWhat the run left behind: runDatabase ({ expiresAt } while the run database is kept, or null), tempPaths, stagingTables. This covers the whole run, not only the nodes in scope.

Node entries ​

Every node in scope has an entry, so a node never goes missing without notice. An entry has these common fields:

FieldMeaning
id, titleThe node's ID and title.
typeThe node's role. It is one of the types below, not the canvas type, so you never inspect operation.
statuscompleted, failed, cancelled or not_run.
stageThe stage index. A not_run node has the stage it was planned to run in.
rows, approximateThe output row count (null when unknown) and whether it is approximate.
modelThe model the node's output rows conform to, by its canonical PDM name vendor/name/Model. Null when the node has no model, or when it cannot be named unambiguously.
schemaThe node's output columns inline, each { name, type }, for a node with no model. It is set exactly when model is null and the node ran in this attempt. It is null for a modeled node (you look its columns up by model), a not_run node, or a node with no recorded figures. An entry never has both a model and a schema.
timingms and scope. A scope of stage means the node shares a fused SQL stage, and ms is the stage's time. A scope of node means the time is the node's own.
blocksTimed blocks of work, each { name, startedAt, ms, attrs }. At most 100 per stage. stages[].blocksDropped counts the overflow.
rejectstotal, byRule, file and fileStatus. See Reject files.
errorClient-safe error text or null.
detailsThe type's own fields.

A not_run node has rows: null and every details field null. Where a field is an array, it is empty instead.

A node's model is resolved when the workflow is compiled:

  • A read has the model it was cast to: the model it names, or its read mapping's target model when it maps.
  • A read that names neither is cast to the model of its source's file-type rules, and has that model. Each path is matched against the enabled rules in order, as the engine matches them, and the first rule it matches gives its modelId. The read has that model when every path's rule names the same one. It is null, and the read detects its columns, when the rules name different models, when a path matches no rule or a rule with no modelId, or when a path is known only at run time: a templated path, a glob, a path expression, or the file a watcher reports.
  • A transform has its output model. A callAction or diffActivity node has the model it names.
  • A write, route or loop has the model of its upstream node. It is null when its inputs have different models, or it has none. One modeled input and one unmodeled input count as different.
  • A node inside a loop body that no edge inside the body leads to has the model of the loop's input, by the same rule.
  • A value, expression, file watcher, webhook or API call node has null.
  • A model named without its module (a bare name) is qualified only when exactly one loaded module defines it. Otherwise it is null.

A workflow that names a model no installed module defines does not compile: the node shows the error Model "vendor/name/Model" is not installed. (or Model "Name" matches more than one installed module. for an ambiguous bare name), and the run is refused before it starts, so a Manifest only ever names installed models.

A node with a null model that ran carries its columns in schema instead.

Each details shape has its own schema. The nodes array is a union on type.

The details objects are closed (additionalProperties: false). Every other object in the document is open. A new details field is therefore a minor version bump, and a 1.0 validator rejects a document that has one. Validate against the schema version you target.

read ​

A dataSourceActivity node whose operation is read. Schema: nodes/read.json.

FieldTypeMeaning
source{ dataSourceId, kind } or nullThe data source the node read from. kind is a plain string such as s3.
filesarrayEach file consumed, with its path and, when the file had a trailer, trailer { removed, declared, actual } (declared and actual are null when unknown). Empty when nothing was read.
decryptedboolean or nullWhether the run read any decrypted input. It is plan-wide: when any read in the run is decrypted, every read entry has true. A run that read decrypted input never gets a reject rows file (withheld).
onCastFailurestring or nullWhat the read did with a value that does not cast to its model's type: fail (the run fails), null (the value is NULL) or reject (a reject row, see Reject files). Null when the read is not cast to a model.
json
{
  "source": {
    "dataSourceId": "ds_s3",
    "kind": "s3"
  },
  "files": [
    {
      "path": "in/custodian.txt",
      "trailer": {
        "removed": 1,
        "declared": 1200,
        "actual": 1200
      }
    }
  ],
  "decrypted": false,
  "onCastFailure": "reject"
}

write ​

A dataSourceActivity node whose operation is write. Schema: nodes/write.json.

FieldTypeMeaning
targetstring or nullWhere the output was published.
formatstring or nullThe output format, such as csv.
publishrename, object, table or nullHow the output was published.
rowsnumber or nullRows written; null when the count was lost.
writtenInAttemptnumber or nullThe attempt that wrote the output.
skippedOnResumeboolean or nullTrue when this attempt skipped the write because an earlier attempt had already done it.
json
{
  "target": "s3://bucket/out.csv",
  "format": "csv",
  "publish": "object",
  "rows": 800,
  "writtenInAttempt": 2,
  "skippedOnResume": false
}

transform ​

A transform node. Schema: nodes/transform.json.

FieldTypeMeaning
evaluatedAssql, js or nullHow the transform ran.
jsRulesarrayOne entry { rule: "*", reason } when the transform ran as JavaScript: the * stands for all its rules, and reason says why it could not run as SQL. It does not list rules one by one. Empty when the transform ran as SQL.
json
{
  "evaluatedAs": "sql",
  "jsRules": []
}

route ​

A route node. Schema: nodes/route.json.

FieldTypeMeaning
branchesobject or nullRows per branch handle.
evaluatedAssql, js or nullHow the conditions ran.
json
{
  "branches": {
    "schwab": 800,
    "default": 400
  },
  "evaluatedAs": "sql"
}

callAction ​

A callAction or activity node. Schema: nodes/callAction.json.

FieldTypeMeaning
modulestring or nullThe module called, as vendor/name. Null when the call names no qualified model.
actionstring or nullThe action called, as Model.action. Null when the call names no qualified model.
callsnumber or nullRequests this stage run sent; null where the stage cannot count them. An item whose parameters did not resolve sends none.
failedCallsnumber or nullCalls that failed.
ledgerAnswerednumber or nullCalls the call ledger answered from its record, without sending the request again: those an earlier worker of the same attempt made before it was lost.
driverRowsnumber or nullRows that drove the calls.
json
{
  "module": "acme/crm",
  "action": "Account.lookup",
  "calls": 8,
  "failedCalls": 0,
  "ledgerAnswered": 0,
  "driverRows": 800
}

loop ​

A loop node. Schema: nodes/loop.json.

FieldTypeMeaning
iterationsnumber or nullItems the loop handled in this attempt: the requests it sent (the one that failed included) plus the calls the ledger answered, so a resumed loop still counts the items an earlier worker handled. An item whose parameters did not resolve is not counted. An input path can make one input row several items. Null when the loop body runs as a control step rather than as one API step.
json
{
  "iterations": 800
}

diff ​

A diffActivity node. Schema: nodes/diff.json.

FieldTypeMeaning
basestring or nullThe base file path as configured on the node, before any template or source path is applied.
addednumber or nullRows in the target file whose key the base file lacks.
removednumber or nullRows in the base file whose key the target file lacks.
changednumber or nullRows whose key is in both files with different values.

The counts cover every difference between the files, even when the node's limit or offset keeps only some of the rows. They are null when the stage could not count them.

json
{
  "base": "out/holdings-yesterday.csv",
  "added": 500,
  "removed": 100,
  "changed": 200
}

fileWatcher ​

A fileWatcher node. Schema: nodes/fileWatcher.json.

FieldTypeMeaning
matchedFilesarray of stringsThe files that triggered the run, by the path the watcher reported. An encrypted file is listed by its own path, never by its decrypted copy. Empty when no file triggered the run.
json
{
  "matchedFiles": ["in/custodian.txt"]
}

ingress ​

A webhook or apiCall node. details is always {}. Schema: nodes/ingress.json.

json
{}

value ​

A value node. details is always {}. Schema: nodes/value.json.

json
{}

expression ​

An expression node. details is always {}. Schema: nodes/expression.json.

json
{}

Attempts and reruns ​

A run that failed, or was cancelled or stopped, can be rerun with POST /workflow/runs/:runId/rerun. A completed or running run cannot. A rerun is a new attempt of the same run: it keeps the run's ID, its parameters and its identity, so run.startedBy is unchanged. run.admittedAt and the {{timestamp}} path token stay the same across attempts.

Each attempt writes its own document, so attempt-1.json stays as it was and attempt-2.json shows the resumed run. attempts lists every attempt so far.

A write node whose output an earlier attempt already wrote is not written again. Its entry has status: "completed" and skippedOnResume: true, and writtenInAttempt says which attempt wrote it.

Reject files ​

Each node in scope with at least one reject gets its own file, at the Manifest path without .json, followed by .rejects.<nodeId>.ndjson. For example, manifests/workflow-abc123/attempt-2.rejects.read1.ndjson. A node with no rejects gets no file.

Each line is a JSON object with the columns rule, target, reason, source_ordinal and source_key. source_key is a JSON object. The node ID is implied by the file name.

A read of a CSV file with a model is cast to it. What happens to a value that does not cast to its column's type is the read node's "On cast failure" setting (onCastFailure):

  • Fail the run (fail, the default): the run fails at the first such value, naming the column and the line of the file.
  • Set NULL (null): the value is NULL in the output and the run goes on. No reject is recorded.
  • Report rejects (reject): the value is NULL in the output, the row is kept, and it is a reject with reason cast_failed, target the column and rule the kind of value (for example as a number). The file is read twice: first for its rejects, then for its output. When more of a column's rows fail than the node's "Fail above" limit (maxRejectRate, a number from 0 to 1, default 0.5) allows, the run fails before anything is written, with an error such as Column "amount" of "Read trades": 60% of 10 rows could not be cast to DECIMAL(18, 2) (limit 50%). 0 fails on any value that does not cast. The reject rows of a run that failed this way are still written, and its Manifest counts them.

The node's entry references the file in rejects.file (path, format, rows). rejects.fileStatus says why there is, or is not, a file:

fileStatusMeaning
writtenThe file was written.
noneThe node had no rejects.
disabledThe Manifest node has "Write reject rows" turned off.
unavailableThe rows could not be read: the run database was gone, or the node did not run, so its rejects are unknown.
withheldThe run read decrypted input, so reject rows are not written to a new plaintext file. The counts are still written.
failedWriting the file failed.

Worked example ​

This document covers one node of every type and validates against manifest.json.

json
{
  "$schema": "https://docs.precept.sh/schemas/run-manifest/1.0/manifest.json",
  "schemaVersion": "1.0",
  "manifest": {
    "nodeId": "m1",
    "title": "Nightly manifest"
  },
  "run": {
    "id": "workflow-abc123",
    "attempt": 2,
    "workflowId": "wf_nightly",
    "startedBy": "auth0|user1",
    "trigger": "rerun",
    "engine": "v2",
    "admittedAt": "2026-10-21T01:00:00.000Z",
    "startedAt": "2026-10-21T01:20:00.000Z",
    "endedAt": "2026-10-21T01:21:05.000Z",
    "state": "completed",
    "error": null
  },
  "attempts": [
    {
      "attempt": 1,
      "state": "failed",
      "startedAt": "2026-10-21T01:00:00.000Z",
      "endedAt": "2026-10-21T01:05:00.000Z"
    },
    {
      "attempt": 2,
      "state": "completed",
      "startedAt": "2026-10-21T01:20:00.000Z",
      "endedAt": "2026-10-21T01:21:05.000Z"
    }
  ],
  "restarts": [
    {
      "generation": 2,
      "at": "2026-10-21T01:20:30.000Z",
      "reason": "worker_lost"
    }
  ],
  "parameters": {
    "asOf": "2026-10-20"
  },
  "scope": {
    "nodeIds": [
      "hook1",
      "watch1",
      "read1",
      "map1",
      "route1",
      "call1",
      "loop1",
      "diff1",
      "val1",
      "expr1",
      "sink1"
    ]
  },
  "totals": {
    "nodes": 11,
    "nodesNotRun": 0,
    "rowsWritten": 800,
    "rejects": 0
  },
  "nodes": [
    {
      "id": "hook1",
      "title": "Inbound webhook",
      "stage": 0,
      "status": "completed",
      "rows": 1,
      "approximate": false,
      "model": null,
      "schema": [
        {
          "name": "body",
          "type": "JSON"
        }
      ],
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "ingress",
      "details": {}
    },
    {
      "id": "watch1",
      "title": "Custodian drop",
      "stage": 0,
      "status": "completed",
      "rows": null,
      "approximate": false,
      "model": null,
      "schema": [
        {
          "name": "path",
          "type": "VARCHAR"
        }
      ],
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "fileWatcher",
      "details": {
        "matchedFiles": [
          "in/custodian.txt"
        ]
      }
    },
    {
      "id": "read1",
      "title": "Custodian file",
      "stage": 0,
      "status": "completed",
      "rows": 1200,
      "approximate": false,
      "model": "acme/custody/Position",
      "schema": null,
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "read",
      "details": {
        "source": {
          "dataSourceId": "ds_s3",
          "kind": "s3"
        },
        "files": [
          {
            "path": "in/custodian.txt",
            "trailer": {
              "removed": 1,
              "declared": 1200,
              "actual": 1200
            }
          }
        ],
        "decrypted": false,
        "onCastFailure": "reject"
      }
    },
    {
      "id": "map1",
      "title": "Normalize",
      "stage": 1,
      "status": "completed",
      "rows": 1200,
      "approximate": false,
      "model": "acme/custody/Holding",
      "schema": null,
      "timing": {
        "ms": 980,
        "scope": "stage"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "transform",
      "details": {
        "evaluatedAs": "sql",
        "jsRules": []
      }
    },
    {
      "id": "route1",
      "title": "Split by custodian",
      "stage": 1,
      "status": "completed",
      "rows": 1200,
      "approximate": false,
      "model": "acme/custody/Holding",
      "schema": null,
      "timing": {
        "ms": 980,
        "scope": "stage"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "route",
      "details": {
        "branches": {
          "schwab": 800,
          "default": 400
        },
        "evaluatedAs": "sql"
      }
    },
    {
      "id": "call1",
      "title": "Enrich",
      "stage": 2,
      "status": "completed",
      "rows": 800,
      "approximate": false,
      "model": "acme/crm/Account",
      "schema": null,
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "callAction",
      "details": {
        "module": "acme/crm",
        "action": "Account.lookup",
        "calls": 8,
        "failedCalls": 0,
        "ledgerAnswered": 0,
  "ledgerAnswered": 0,
        "driverRows": 800
      }
    },
    {
      "id": "loop1",
      "title": "Per account",
      "stage": 2,
      "status": "completed",
      "rows": 800,
      "approximate": false,
      "model": "acme/crm/Account",
      "schema": null,
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "loop",
      "details": {
        "iterations": 800
      }
    },
    {
      "id": "diff1",
      "title": "Against yesterday",
      "stage": 3,
      "status": "completed",
      "rows": 800,
      "approximate": false,
      "model": "acme/custody/Holding",
      "schema": null,
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "diff",
      "details": {
        "base": "out/holdings-yesterday.csv",
        "added": 500,
        "removed": 100,
        "changed": 200
      }
    },
    {
      "id": "val1",
      "title": "Cutoff",
      "stage": 3,
      "status": "completed",
      "rows": 1,
      "approximate": false,
      "model": null,
      "schema": [
        {
          "name": "cutoff",
          "type": "DATE"
        }
      ],
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "value",
      "details": {}
    },
    {
      "id": "expr1",
      "title": "Fee",
      "stage": 3,
      "status": "completed",
      "rows": 800,
      "approximate": false,
      "model": null,
      "schema": [
        {
          "name": "account",
          "type": "VARCHAR"
        },
        {
          "name": "fee",
          "type": "DOUBLE"
        }
      ],
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "expression",
      "details": {}
    },
    {
      "id": "sink1",
      "title": "Schwab out",
      "stage": 4,
      "status": "completed",
      "rows": 800,
      "approximate": false,
      "model": "acme/custody/Holding",
      "schema": null,
      "timing": {
        "ms": 120,
        "scope": "node"
      },
      "blocks": [],
      "rejects": {
        "total": 0,
        "byRule": {},
        "file": null,
        "fileStatus": "none"
      },
      "error": null,
      "type": "write",
      "details": {
        "target": "s3://bucket/out.csv",
        "format": "csv",
        "publish": "object",
        "rows": 800,
        "writtenInAttempt": 2,
        "skippedOnResume": false
      }
    }
  ],
  "stages": [
    {
      "index": 1,
      "status": "completed",
      "nodeIds": [
        "map1",
        "route1"
      ],
      "ms": 980,
      "peakBytes": 0,
      "spillBytes": 0,
      "blocksDropped": 0,
      "error": null
    }
  ],
  "leftovers": {
    "runDatabase": null,
    "tempPaths": [],
    "stagingTables": []
  }
}