docs / plm / Reviews and workflows

Reviews and workflows

The server has two ways to control how a revision gets released: review rounds, built into every revision, and workflows, processes an administrator draws for anything more involved. Both can be used together.

Review rounds

Submitting a revision for review moves it to In review and opens a review round on it. The round holds its reviewers, how many approvals it needs, whether the submitter's own approval counts, an optional due date, and every decision. When submitting you can add reviewers, set a due date and post a first comment.

The review rule

How many approvals a release needs, and from whom, is the review rule in Server settings: the number of approvals, a due period, the reviewers (users and groups, written user:grace or group:quality; with none, the checkin group decides) and whether self-approval counts. Overrides give a different rule to a category (and everything beneath it) or to a part type; the nearest category override wins, then the part type's.

The default rule needs 0 approvals, so until an administrator sets one a draft can be released directly. The rule is copied into a round when it opens, so a settings change applies from the next submission.

The inbox

Inbox lists what waits on you (rounds where you are a reviewer, workflow tasks assigned to you) and what you submitted. Its badge in the top bar counts what waits on you. Review requests can be approved or rejected directly from the inbox.

There is no built-in email. To notify people, an administrator writes a review-event.js script, which hears every review event and can post to a chat or mail service (see Scripts).

Workflows

Workflows lists the processes you may start and the runs you started or take part in. Administrators build process definitions in Administration → Workflow editor.

Drawing a process. Drag steps from the palette onto the canvas and connect an output to an input. Each connection's outcome is always, success, reject or a custom outcome a script returns. A process needs one Start and at least one End, every step reachable from Start, and no cycles. A fork runs its branches independently; a join waits for every incoming path to finish with a matching outcome, so a rejected review cannot pass a join and release a model.

Steps include:

Forms and reviews arrive as Inbox tasks. The run's page shows its progress, the decisions, and a log of who did what, which steps ran or were skipped, and what changed. Script output and errors are shown only to administrators.

Versions. Saving a definition makes a new version. A run keeps the version, graph, assignees and fields it started with, so editing a definition affects only later runs. A definition can be made inactive, limited to some part types, and limited to the users or groups who may start it. With no start list, authors may start it; a list can let other groups submit forms without being able to edit models.

Release only through the workflow. Marking a definition Require this workflow for part release stops direct release of the part types it applies to: the release must come from that workflow's release step. A workflow containing a model review pins the document it reviewed; editing the model means starting a fresh process.

Script steps define step(input) and return { outcome: "success", fields: {...} }, a custom outcome, or false for reject; throwing fails the step. The input carries the run, step, part, revision, document hash, field values, initiator, attempt number and a stable idempotencyKey (runId:nodeId). A step that calls an outside system (an ERP order, a vendor upload) is committed as dispatched before it runs, so it is never run twice by accident. If the server restarts mid-step, the step is marked failed rather than silently run again. Administrators retry failed steps from their inbox; give the receiving system the idempotency key so a retry cannot double an order. The initiator or an administrator can cancel a run while no automatic step is executing.

Scripts

Administrators customise the server with JavaScript files in the scripts directory (--scripts, default <data>/scripts). The directory is the source of truth: keep it in git if you like. The server re-reads a file when it changes, so a git pull takes effect on the next call without a restart. Administration → Scripts edits the same files in the browser and test-runs them.

FileFunctionRuns
named by a Script-mode part typepartNumber(input)creating a part of that type
revision-label.jsrevisionLabel(input)creating any revision; null accepts the typed label or the suggestion
before-release.jsbeforeRelease(input)before a release commits; a throw refuses the release
after-release.jsafterRelease(input)after a release commits; a throw becomes a warning
reviewers.jsreviewers(input)submitting a revision for review
review-event.jsreviewEvent(input)after every review event

Each function takes one JSON argument and returns JSON. If a hook throws, the operation is refused and the user sees the thrown message, except where the table says otherwise.

Scripts are fully trusted. They can make HTTP requests (http.request), read and write files (fs), run programs (process.run), and read and change PLM data (plm.*). An administrator who can edit scripts in the browser can therefore run any program on the server. Turn the editor off in Server settings, or start the server with --lock-script-editor so no administrator can turn it back on. Scripts keep running from the directory either way.

A test run is a real run: test-running a script that posts to your ERP really posts.

Loops are limited to ten million iterations and calls to a depth of 512, and an HTTP request times out after 30 seconds unless it asks for another limit. There is no overall time limit on a script, and a program it runs is not limited at all.

A notification takes a few lines:

function reviewEvent(input) {
  if (input.event !== 'opened' && input.event !== 'reject') return;
  const s = input.subject;
  http.request({
    url: 'https://chat.example.com/hooks/plm',
    method: 'POST',
    body: { text: `${s.number} rev ${s.label}: ${input.event} by ${input.user.username}` },
    timeoutMs: 5000,
  });
}