Process-Flow Platform Blocks
Four host-executed workflow blocks for creating process-flow tasks, spawning instances, assigning personas, and writing governed table rows — the building blocks of automated ingest pipelines.
The process-flow platform blocks are first-party workflow blocks that run with host-level database and network access — they cannot run inside a sandboxed vendor extension. Use them when a workflow needs to interact directly with the process-flow or managed-table subsystem.
Table Write, Create Process Instance and Assign Persona appear in
the workflow editor's block gallery under the Core category. Create
Process Task is hidden from the gallery (hideFromToolbar) and is added by
a template or by editing the workflow definition — it still runs exactly like
the others. All four are available in every workflow, regardless of which
extensions the organization has enabled.
Referencing an output. Every output below is a top-level field of the
block, read as <blockname.field> with the block's name lowercased and its
spaces removed: a Create Process Instance block named Open case is
<opencase.instanceId>. Earlier versions of the editor offered a single
response output for these four blocks; a reference such as
<opencase.response.instanceId> never resolved and reached the next block as
literal text. Reference the field directly.
The same four as agent tools. An Agent block or a Cortex room can call these
four as tools too. Each declares the inputs its block takes, so a model is shown
which fields are required and the limits on each one. It is not shown an empty
object. A fifth, read-only tool,
list_process_flow_templates,
lists the templates a model may start. A Cortex room runs it without asking.
It asks before any of the four that write.
create_process_task — Create Process Task
Creates a new task inside an existing process-flow instance. Use this when a workflow is triggered by an external event (calendar, webhook, schedule) and needs to add a task to a flow that is already running.
Block type: create_process_task
Sub-blocks:
| Sub-block | Required | Meaning |
|---|---|---|
processInstance | Yes | The process-flow instance to add the task to. Populated from a combobox listing live instances in the current workspace environment. Can also be set via a block reference (e.g. <start.instanceId>). |
taskName | Yes | Display name for the new task. Supports block references to the workflow trigger's output (e.g. <start.event.title>). |
attributes | No | A JSON object of extra attributes to store on the task (e.g. { "scheduledDate": "<start.event.start>" }). It must be a JSON object. An array, a number or a boolean is refused with "Attributes must be a JSON object"; it is no longer stored as an empty object. Leave it blank for no attributes. Keys starting with _ are reserved for the platform and are ignored. |
Output: { taskId, instanceId, stageName } when successful; { error } when not.
Sentinel errors:
| Sentinel | Meaning |
|---|---|
create_process_task_organization_required | The workflow execution context does not carry an organization. |
Other failures return a sentence describing the problem rather than a sentinel.
Example (calendar trigger → add a board-extension review task):
import { defineWorkflow, defineBlock, defineEdge } from '@scrydon/sdk-authoring/workflows'
export const calendarToTask = defineWorkflow({
slug: 'calendar-to-board-review-task',
name: 'Calendar → Board Review Task',
version: '1.0.0',
definition: {
blocks: {
'block-start': {
id: 'block-start',
type: 'starter',
name: 'Start',
position: { x: 0, y: 0 },
subBlocks: {},
outputs: {},
enabled: true,
},
'block-create-task': {
id: 'block-create-task',
type: 'create_process_task',
name: 'Create board review task',
position: { x: 240, y: 0 },
subBlocks: {
processInstance: {
id: 'processInstance',
type: 'combobox',
value: '<start.instanceId>',
},
taskName: {
id: 'taskName',
type: 'short-input',
value: 'Board extension review — <start.event.title>',
},
attributes: {
id: 'attributes',
type: 'code',
value: '{ "scheduledDate": "<start.event.start>" }',
},
},
outputs: { taskId: 'string', instanceId: 'string', stageName: 'string' },
enabled: true,
},
},
edges: [
{ id: 'e1', source: 'block-start', target: 'block-create-task' },
],
loops: {},
parallels: {},
},
})table_write — Table Write
Writes one or more rows to a governed managed table bound to an ontology object type. The block resolves the object type to its silver table at run time, then dispatches the write through the api-table path. No user interaction is required — designed for automated ingest pipelines.
Block type: table_write
Sub-blocks:
| Sub-block | Required | Meaning |
|---|---|---|
objectType | Yes | Ontology object-type slug identifying the target managed table. The block resolves the silver binding at run time. |
mode | Yes | "upsert" (match on identity column, update or insert) or "append" (always insert a new row). |
rows | Yes | A JSON array of { [columnSlug]: value } objects, 1–500 rows per call. A code sub-block can produce this as a JSON string. |
Output: { written: <row count>, tableId: "<uuid>" }.
Sentinel errors (returned as { success: false, error: "<sentinel>" }):
| Sentinel | Meaning |
|---|---|
table_write_organization_required | The workflow execution context does not carry an organization. |
table_write_target_unresolved | objectType is not bound to a silver table in this workspace. Verify the ontology type exists and is bound to a managed table. |
table_write_org_mismatch | The resolved table belongs to a different organization than the running workflow. This should not happen in normal usage; contact support. |
table_write_user_required | The workflow execution context does not carry a user identity. A run started by a person — the play button, an API call, or a webhook on a workflow a person owns — satisfies this. |
table_write_execution_grant_required | The run has a person behind it but carries no execution grant, so it has no authority to write. A run started the normal way — the play button, an API call, or a webhook on a workflow a person owns — always carries one, so seeing this means the run was started outside the usual entry point. A workflow installed from an extension does not hit this: it has no person by design, and resolves its binding through the platform instead. |
Example (webhook ingest — writing a track observation):
'block-write-observation': {
id: 'block-write-observation',
type: 'table_write',
name: 'Write track observation',
position: { x: 480, y: 0 },
subBlocks: {
objectType: {
id: 'objectType',
type: 'short-input',
value: 'track_observation', // ontology object-type slug
},
mode: {
id: 'mode',
type: 'dropdown',
value: 'upsert',
},
rows: {
id: 'rows',
type: 'code',
// JSON array returned by a preceding Function block named "Parse payload":
value: '<parsepayload.result>',
},
},
outputs: { written: 'number', tableId: 'string' },
enabled: true,
},The object type must be bound to a managed table in the workspace environment where the workflow runs. If the object type ships in the same Extension, the Extension must be installed before the workflow runs. The write is attributed to the workflow execution's user identity — workflows run by a service account satisfy the identity requirement automatically.
create_process_instance — Create Process Instance
Creates a new process-flow instance from an extension-declared template, optionally
seeding it with a context object and a dedupeKey. When a dedupeKey is
set, the block is idempotent: a second call with the same key returns the
existing live instance instead of creating a duplicate.
Block type: create_process_instance
Sub-blocks:
| Sub-block | Required | Meaning |
|---|---|---|
extensionId | Yes | The package.id of the Extension that ships the target process-flow template. |
processFlowSlug | Yes | The slug of the process-flow template within that Extension. |
name | Yes (1–200 chars) | Human-readable name for the created instance. Supports template references. |
context | No | A JSON object seeded into the instance as its context. It is shown read-only on the instance page under Run context, and map actions can focus a key with focusAttribute. Agent steps receive only the keys the template declares in attributes: see Instance attributes. A code sub-block can produce a JSON string. |
dedupeKey | No (1–500 chars) | When set, enforces at most one live instance per key in this workspace environment. Concurrent calls with the same key converge to a single instance. Left blank (or only whitespace), it takes the trigger's own dedupeKey when the trigger payload carries one — SharePoint and OneDrive file arrival do — except inside a Loop, where it stays blank. Without either, every call starts a new instance. Keys starting with ontology-watch-detection: are reserved for the platform's detection dispatch and are refused. |
detection | No | A TrackDetectedEvent-shaped JSON object. Accepted and validated; on a valid parse the platform publishes a thin signal to the org-events realtime topic (this is what powers live operational pictures such as Horizon) — leave it unset if you do not need live-event extension. The pre-rename key copDetection is still accepted as a legacy alias; new workflows should use detection. |
Deduplication contract. The dedupe key is enforced at the database level
— a partial unique index on (workspaceEnvironmentId, dedupeKey) covering
only non-terminal instances. Concurrent webhook deliveries for the same key
always converge to one instance; no application-level race is possible. A
terminal instance (completed or cancelled) releases the key, allowing a
fresh instance to be created. The ontology-watch-detection: prefix is a reserved
namespace: a call whose dedupeKey starts with it fails with a message asking
for a different key, because the platform treats an instance in that namespace as
the response to the ontology detection named in its context.
Outputs:
| Output | Type | Meaning |
|---|---|---|
instanceId | string | The instance created, or the existing live one. |
deduped | boolean | true when the call returned an existing live instance rather than creating a new one. |
knowledgeBaseId | string | null | The instance's knowledge base. On deduped: true it is the existing instance's knowledge base. null when the instance has none. |
knowledgeBaseFolder | string | null | The instance's own folder inside that knowledge base, as a folder path (instances/<date>-<run name>-<id>). The same value as <start.processFlow.knowledgeBaseFolder> inside the run's own steps. null when there is no knowledge base. |
A flow that declares knowledge.scope: "template" shares one knowledge base
across its runs, so knowledgeBaseId is the same for every run and
knowledgeBaseFolder is what keeps one run's pages apart from another's.
Sentinel errors:
| Sentinel | Meaning |
|---|---|
create_process_instance_no_environment | The workflow execution context does not carry a workspace environment id, or workspace resolution failed. |
create_process_instance_invalid_context | context is not a valid JSON object. |
create_process_instance_user_required | The workflow execution context does not carry a user identity. |
create_process_instance_organization_required | The workflow execution context does not carry an organization. |
create_process_instance_invalid_input | The block's inputs do not match its schema. The error message names each problem. |
create_process_instance_invalid_detection | detection is not a valid JSON object. |
Called as a tool. A model passes extensionId and processFlowSlug exactly
as list_process_flow_templates returns them, so it should call that tool first.
A model is not shown context: the Agent tool projection hides that field name
from every model-facing schema. A run a model starts therefore has no seed
values. To seed one, use the block.
Example (webhook ingest — open a response instance per detected track):
'block-create-instance': {
id: 'block-create-instance',
type: 'create_process_instance',
name: 'Open response instance',
position: { x: 720, y: 0 },
subBlocks: {
extensionId: {
id: 'extensionId',
type: 'short-input',
value: 'scrydon-drone-response',
},
processFlowSlug: {
id: 'processFlowSlug',
type: 'short-input',
value: 'drone-response',
},
name: {
id: 'name',
type: 'short-input',
value: 'Track <start.body.trackId>',
},
context: {
id: 'context',
type: 'code',
value: '{ "trackRef": "<start.body.trackId>" }',
},
dedupeKey: {
id: 'dedupeKey',
type: 'short-input',
value: '<start.body.trackId>', // one live instance per track
},
},
outputs: {
instanceId: 'string',
deduped: 'boolean',
knowledgeBaseId: 'string',
knowledgeBaseFolder: 'string',
},
enabled: true,
},Writing into the new run's knowledge base
Pass the two knowledge-base outputs to a Knowledge block set to Add Data
(create_document). Use the block's Advanced mode for the knowledge base,
because the id comes from a reference rather than the picker. With the Create
Process Instance block named Open case:
'block-file-complaint': {
id: 'block-file-complaint',
type: 'knowledge',
name: 'File complaint',
advancedMode: true, // the reference field below is read only in Advanced mode
subBlocks: {
operation: { id: 'operation', type: 'dropdown', value: 'create_document' },
knowledgeBaseIdManual: {
id: 'knowledgeBaseIdManual',
type: 'short-input',
value: '<opencase.knowledgeBaseId>',
},
folderPath: {
id: 'folderPath',
type: 'short-input',
value: '<opencase.knowledgeBaseFolder>',
},
name: { id: 'name', type: 'short-input', value: 'Original complaint' },
content: { id: 'content', type: 'long-input', value: '<start.body.message>' },
},
enabled: true,
},The document lands in the run's own folder, so the run's steps can ground on it
and a knowledge.scope: "template" flow keeps it apart from other runs. This
also works when the call was deduplicated: the document is filed with the run
that already exists. See Knowledge for the Folder (path
scope) rules.
Seeding free text into context. A reference inside a string in the
context JSON is inserted as that string's text, escaped: a double quote, a
backslash or a line break in an email body stays inside the value and cannot
add a key. Write the references straight into the JSON:
{ "subject": "<start.body.subject>", "body": "<start.body.message>" }A reference standing on its own as a value ({ "order": <start.body.order> })
is inserted as JSON, and a context that is exactly one reference to an
object is passed on intact. See
References in a JSON field.
extensionId and processFlowSlug must match a template that is already installed
in the organization. The block instantiates an existing template — it does not
install the Extension. Install the Extension first (Settings → Platform → Extensions), then deploy the
ingest workflow.
assign_persona — Assign Persona
Seats users on one persona of a running process-flow instance. Use this when the workflow can work out who should play a part in this particular case — the reviewers whose domain matches a document, the on-call owner, the account team for a customer — instead of stopping to wait for someone to fill in a form.
Block type: assign_persona
A persona is not a label. Its holders decide who may approve a stage transition, who is notified about work waiting on it, and the identity a step executes as — which in turn decides the clearance its knowledge-base reads run under. Assigning the wrong person grants real access, so this block validates rather than guesses.
Two consequences worth knowing before you author against it:
- The block is authorized, not merely scoped. A workflow does not inherit
authority over every instance in its organization. The block is checked
as the user the run acts as — the verified run identity, the same one
every other governed tool call in the run uses — against the same check an
operator faces when assigning personas by hand. That user must be allowed
to manage personas on the target instance, or you get
assign_persona_forbidden. - A run cannot seat its own acting identity. That is a separation-of-duties
refusal (
assign_persona_self_assignment), independent of authorization — otherwise a workflow author could make themselves the approver of their own workflow, and the approval gate only checks that the approver is a holder. - A workflow never staffs the approvers of its own instance. If the
persona decides an approval on the target instance — a stage's
approverPersona, an approval action's persona, the reviewer role of a human-in-the-loop gate in any deployed workflow the instance runs (even one that has not paused yet), or an approval the instance is waiting on right now — the block refuses (assign_persona_approval_persona), for seating and for unseating. Otherwise a run could fill a quorum with one accomplice, or remove the dissenters. An operator can still staff these personas from the instance page; the refusal applies to workflows only. - Every change and every refusal is in the instance's activity log, marked via workflow, so a run's change never reads as a person's own edit.
Sub-blocks:
| Sub-block | Required | Meaning |
|---|---|---|
processInstance | Yes | The instance to assign on. Takes a block reference, so the instanceId output of a Create Process Instance block named Open review is <openreview.instanceId>. ({{…}} is the syntax for workspace environment variables, not block outputs.) |
personaSlug | Yes (1–50 chars) | A persona slug declared by the instance's process-flow template. An undeclared slug is an error, not a silent no-op. |
mode | Yes | replace (default) — the listed users become this persona's holders. add — they are added, existing holders keep their seats. |
userIds | Yes | A JSON array of user ids, at most 50. A value wired from another block arrives as a JSON string and is parsed. An empty array with mode: "replace" clears the persona — but a whitespace value is an error, not a clear, so an upstream block that produced nothing cannot silently unseat everyone. |
Scope of replace. It applies to the one persona named, never the whole
cast. Setting a reviewer cannot wipe the approver.
Who is assignable. Only members of the instance's organization. A user id
that is not a member is skipped and reported in skipped — the other users
are still seated. When membership cannot be checked at all, the user is skipped
as membership_unknown instead, never seated. If no requested user is assignable the block fails instead
of quietly assigning nobody.
Cardinality is enforced. A persona declared cardinality: "one" refuses a
second holder (in either mode), and a cardinality: "system" persona refuses
every holder — it has none by design.
Output:
{
"assigned": 2,
"assignedUserIds": ["user_a", "user_b"],
"removedUserIds": ["user_c"],
"skipped": [{ "userId": "user_d", "reason": "not_in_organization" }],
"error": null
}Branch on skipped. Nothing downstream will tell you a persona is
under-staffed. A human-in-the-loop quorum gate clamps its threshold to
whoever is actually seated, so a panel of eight with seven holders silently
becomes seven-of-seven; a distribution action instead fails outright. This
block's output is the only place the shortfall is named.
Skip reasons:
| Reason | Meaning |
|---|---|
not_in_organization | The organization answered, and the user is not a member of it. |
membership_unknown | The membership check could not be completed (api-platform unreachable). The user is not seated — and if they already held the persona they keep it, because a failed lookup is not evidence about a person. |
already_assigned | mode: "add" and the user already holds this persona. Not an error; the call is idempotent. |
duplicate_in_request | The same user id appeared more than once for this persona. |
blank_user_id | The id was empty or whitespace. Reported rather than dropped — see the userIds note above. |
Sentinel errors:
| Sentinel | Meaning |
|---|---|
assign_persona_forbidden | The caller is not authorized to assign personas on this instance. A workflow does not inherit authority over every instance in its organization. |
assign_persona_approval_persona | The persona decides an approval on this instance (see the callout above). A workflow may not change who holds it, in either direction. Have an operator staff it, or assign a persona that does not approve. |
assign_persona_self_assignment | The userIds include the run's own acting identity. Refused as separation of duties: a workflow may not make its own author the approver of it. Compute the set excluding the author, or have an operator seat them. |
assign_persona_unknown_persona | personaSlug is not declared by this instance's template. Check the template's personas[]. |
assign_persona_persona_not_assignable | The persona is declared cardinality: "system" and never has human holders. |
assign_persona_cardinality_violation | The persona declares cardinality: "one" and the call would leave it with more than one holder. |
assign_persona_no_assignable_users | Every requested user was skipped. The response's skipped names each one and why. |
assign_persona_instance_not_found | No such instance reachable from this run. Also returned for an instance in a different organization or a different workspace environment — existence is never confirmed across a tenant or workspace boundary. |
assign_persona_instance_terminal | The instance is completed or cancelled. |
assign_persona_invalid_user_ids | userIds is not a JSON array of strings. A comma-separated list is deliberately not accepted — it would seat one user whose id contains brackets. Whitespace is also rejected rather than treated as a clear. |
assign_persona_too_many_users | More than 50 user ids. |
assign_persona_no_organization | The workflow execution context does not carry an organization. |
assign_persona_user_required | The run carries no verified user identity in the instance's organization. That identity is recorded as the assigner, so the block refuses rather than guessing one. |
Auditing. Every change writes a persona_assigned row to the instance
activity log with the holders before and after, the mode, the shortfall, and the
workflow execution that made it. A refused approver change writes a
persona_assignment_refused row instead — the personas and users it asked
for, and the run that asked — so an attempt is visible even though nothing
changed. Read both with GET /api/process-flows/{id}/activity-log.
After a write. The new holders are notified about the open tasks already waiting on the persona, and a distribution step that failed because nobody held its persona goes back to pending — the same follow-ups as an assignment made from the instance page.
{
id: 'assign-reviewers',
type: 'assign_persona',
name: 'Assign Reviewers',
inputs: {
processInstance: '<openreview.instanceId>',
personaSlug: 'reviewer',
mode: 'replace',
userIds: '<pickpanel.reviewerIds>',
},
enabled: true,
},list_process_flow_templates — List Process Flow Templates
A read-only agent tool, not a block. It lists the process-flow templates
added to the run's workspace, so a model can name a real template to
create_process_instance instead of guessing one.
Input: none. The workspace is always the run's own. An input cannot point the read at another workspace.
Output: { templates: [...] }. Each entry has the following fields:
| Field | Meaning |
|---|---|
extensionId | Pass as create_process_instance's extensionId. |
processFlowSlug | Pass as create_process_instance's processFlowSlug. |
name | The template's display name. |
description | The template's description, or null. |
extensionName | The display name of the extension that ships the template. |
stageCount | The number of stages in the template. |
personas | [{ slug, displayName }]. These are the slugs assign_persona accepts. |
The list answers what the workflow editor's template picker answers for the same person in the same workspace. It uses the same authorization check and lists only the templates added to the workspace.
Sentinel errors:
| Sentinel | Meaning |
|---|---|
list_process_flow_templates_forbidden | The run's user may not read this workspace's templates. |
list_process_flow_templates_authorization_unavailable | The authorization service did not answer. Retry. |
list_process_flow_templates_user_required | The run carries no verified user identity in this organization. |
list_process_flow_templates_workspace_not_found | The run's workspace environment does not resolve to the user's workspace in this organization. |
list_process_flow_templates_no_environment | The run carries no workspace environment. |
list_process_flow_templates_organization_required | The run carries no organization. |
list_process_flow_templates_invalid_input | The arguments are not a JSON object. The tool reads no field, so any object is accepted. |
Building an ingest pipeline
The blocks compose naturally into a webhook-driven ingest pipeline that opens a flow and staffs it, so a machine-started instance is not inert:
[Webhook trigger]
↓
[Function block — parse payload, build rows]
↓
[table_write — write observation rows]
↓
[create_process_instance — open response flow, deduped by trackId]
↓
[Function block — work out who should review this case]
↓
[assign_persona — seat that set on the "reviewer" persona]Without the last two steps the instance exists but nobody holds a persona on it, so it appears in nobody's worklist and its approval gates have no approvers. (A new instance does inherit the cast of the previous instance of the same template when there is one, so this matters most for the first run of a flow and whenever the right people depend on the case.)
A full worked example ships as part of the drone-response C2 extension (see the Extension examples directory). The Extension includes the ontology (Track / TrackObservation object types), the ingest workflow, and the response process flow — install them in one upload and the webhook URL is live immediately.