Skip to content

Boards runtime walkthrough

This walkthrough operates the Support triage board built in the Boards configuration walkthrough: discover the board, create a ticket directly onto it, and move the item through the process. Everything uses the public integration API, so it works with any authenticated caller the board permissions allow — including a service-account key.

All examples were executed against a real Moltaro installation; response bodies are real, trimmed for length. The complete contract is under the Boards runtime tag of the API reference.

The workspace owner supplies the guide, URL, key, and permissions through the Portal or administrative handoff. An agent does not create or elevate its own account. Apply the row-version, retry, and verification rules in Reliable API automation.

Board access has two independent gates. Boards:ACCESS admits the caller to the module and Board shell. Every returned Board Item must also satisfy that Board’s BoardItem.View Security Permission. Moltaro applies the View predicate before count, sorting, cursor or offset, and page limit, so totals and cursors describe only items the caller may read. Status columns remain visible process structure; BoardStatus.View is not an additional Board Item predicate.

An ordinary item mutation requires pre-change BoardItem.View plus its exact Permission. A move also requires MoveItem; when it uses a configured transition it additionally requires UseTransition:<transition-key>. Board Data updates require EditBoardData and Field:Write for every changed field. Adding an item evaluates AddItem against the proposed status, target, Board Data and due date, applies Initial Assignment Rules, and requires final BoardItem.View before commit.

Target values are not disclosed by a successful Board Item predicate alone. Projection requires BoardItem.View, Board Target:View, the target resource’s own View, and that resource’s field-safe projection. Typed target(...) paths inside a Security Statement are predicate facts and do not themselves grant target access.

Calling Boards from workspace C# automation

Section titled “Calling Boards from workspace C# automation”

For an Action, TriggerHandler, Command, Job, or HttpEndpoint in the Net Operation Project, prefer IBoardAutomationCommandService and IBoardAutomationQueryService from Moltaro.Package.NET.Boards.Automation. They are application-level facades: AddItemAsync creates Board Data internally and performs target/repeat, Security and transition rules, audit/business events, resource events/outbox, and attention projections. The caller supplies a BoardRuntimeTargetRef and a caller-owned UUID OperationKey, not BoardRuntimeActor or DataRecordId.

Ordinary calls authorize and audit the original actor captured by the host. Only a runtime-confirmed trusted system invocation may use moltaro-system-automation; naming that actor in package code does not create trusted provenance. Both actor forms evaluate the same Board Security Statements and exact Permissions, and the system actor receives no implicit access. Calls execute in a separate application scope/transaction, and a successful return means that main transaction committed. Ordinary facade mutations deliberately have the same add/repeat semantics as the Runtime API and do not create an automation receipt. After an unknown ordinary add outcome, query the open item before deciding whether to start a new logical admission. Retry the exact same add with its original OperationKey; an exact replay returns the original item without a duplicate, while changed-payload reuse returns an idempotency conflict. The replay scope includes the captured actor, board, and target definition and does not bypass authorization. When a failed facade result should fail the containing function, call result.ThrowIfFailed(). This raises a bounded SDK exception that preserves the first platform error code and its safe message in Function Operations; wrapping Errors in a generic exception discards that reason code.

Clients built from the earlier add-item contract must regenerate from the current OpenAPI document and replace MutationIdempotencyKey with the required UUID OperationKey. Arbitrary string keys are no longer accepted. Package authors must also rebuild against the current Boards Package SDK and pass the UUID as the first AddBoardAutomationItemRequest constructor argument. Call a Board command before staging or saving Entity or owned-table changes in the same Net Operation Project transaction. Otherwise the facade fails before opening its separate scope with moltaroBoards.runtime.entityMutationBeforeBoardCommandUnsupported; start a new transaction rather than retrying from the Entity-first transaction. Enabled Board event Constraints are evaluated inside that transaction. A rejection preserves every ordered error’s code, workspace-authored message, target, and safe metadata; it commits no partial Board Item, Board Data, Target, history, audit, resource event, notification, or outbox effect. Historical import and reconciliation are different: they do not replay transition, entry, or exit Constraints against today’s actor or clock. They do enforce the resulting status invariant before committing the imported or reconciled state. The complete lifecycle-to-trigger matrix, including same-status reorder, remove, terminal reopen, new pass, administrative relocation, and Board activation behavior, is documented in Board constraints: Which bindings run for each lifecycle operation. Use the returned RowVersion for the next mutation, and poll any returned function job id at GET /api/workspace/functions/jobs/{jobId}. Validation and BeforeSaveMutation phases reject the facade with moltaro.automation.executionPhase.unsupported.

EntityDefinition.TagsEnabled and Board.TagsEnabled are independent. The first controls tags whose owner is an EntityInstance; the second controls tags whose owner is a BoardItem. An Entity-backed Board does not inherit the Entity Definition setting, and enabling one does not enable the other.

Ordinary tag mutation accepts an authorized open BoardItem. A terminal item returns a correlated safe Problem Details response with code moltaro.tags.boardItem.terminalRequiresHistoricalOperation and metadata that names ReconcileHistoricalItemTagsAsync. Disabled Entity and Board owners use moltaro.tags.entity.disabled and moltaro.tags.board.disabled; permission and unknown-owner failures use moltaro.tags.access.denied and moltaro.tags.resource.notFound. Consult the installation-local OpenAPI for the exact HTTP schema.

Use ImportHistoricalItemsAsync for an initial or corrective import whose source provides a stable per-item identity. One call accepts 1–500 ordered items; each item commits in its own short transaction, so the batch can return both successes and stable item errors. SourceKey is case-sensitive, at most 128 characters, and cannot contain whitespace or control characters. Moltaro stores one durable identity per Board and source key. Ordinary historical import does not adopt a Board item that already exists without that identity. The item’s DataRecordId is fixed when the item is created; reconciliation updates fields in that same Board Data record and rejects attempts to replace the record identity.

Create requests may start directly in a terminal status. IsOpen comes only from the configured status type; a terminal item defaults CompletedAtUtc to its effective entered time and rejects completion before that time, while a non-terminal item rejects a completed timestamp. Reconciliation applies the same chronology rule after preserving omitted timestamps. Null content and due values mean “absent” on create and “preserve” on reconcile. A JSON null inside a supplied Fields patch clears that one field.

An exact replay returns Unchanged without requiring a row version and writes no new Board Data, item, history, audit, or resource event. A changed identity requires its current ExpectedRowVersion; missing or stale tokens return a conflict. After cancellation or an unknown response, call GetHistoricalImportAsync(boardIdOrKey, sourceKey) or safely replay the whole batch. Keep source keys stable across retries and inspect every ordered item result, including FollowUpOperationIds.

Reconcile Board Data on terminal historical items

Section titled “Reconcile Board Data on terminal historical items”

Use ReconcileHistoricalItemDataAsync when an already imported historical item is terminal and only its Board-owned data is wrong. This is a trusted Net Operation Project migration operation; it does not relax UpdateDataAsync or the Runtime API rule that ordinary callers cannot edit a closed item.

One request accepts 1-500 ordered patches and commits each item independently. Each patch must provide exactly one locator: either BoardItemId or the case-sensitive historical SourceKey. The item must still belong to that Board, have a durable BoardHistoricalImportIdentity, be non-removed, and be closed in a terminal status. ExpectedRowVersion is mandatory for every patch. A changed patch with a stale token is rolled back; an exact replay is recognized before the token check, returns Unchanged with the current row version, and remains write-free.

Omit Subject or Description to preserve it. Supply new BoardHistoricalTextPatch(null) to clear it, or wrap a string to replace it. Fields accepts only fields from the Board-owned Part definition; JSON null clears one supplied Part field. The normal contained-Entity path still enforces field schema, rules, audit, presentation/cache maintenance, and the Board UpdateData business invariant.

A successful change keeps the same Board item and Board Data record and changes only Board Data plus the Board item’s RowVersion/ordinary modified stamp. Status, open/closed state, rank, run, entered/completed/due timestamps, target, responsibilities, move history, transition history, and Board lifecycle audit are not rewritten. The Board Data audit diff records moltaro-system-automation and preserves the original function run, function version, original user, and correlation metadata. Inspect every ordered item result; after an unknown outcome, safely replay the same desired patch.

Reconcile tags on terminal historical items

Section titled “Reconcile tags on terminal historical items”

Use ReconcileHistoricalItemTagsAsync only from trusted Net Operation Project automation when a terminal BoardItem itself must own provenance or classification tags. It does not relax the ordinary Tags API. One request accepts 1-500 ordered items and commits each owner independently; inspect SucceededCount, FailedCount, and every item error because there is no cross-owner transaction.

Each item provides exactly one BoardItemId or case-sensitive historical SourceKey, plus ExpectedRowVersion. Add creates only missing names and leaves an existing same-name tag unchanged. ReplaceManagedSet requires the complete ManagedTagNames allow-list owned by that synchronization and can update or remove only those names; an empty desired Tags list removes all currently stored names in that managed set. All tags outside the managed set are preserved, including manual and unrelated automation tags. Exact replay returns Unchanged without writes even after the supplied token became stale; a real change with a stale token fails.

The operation requires an active, tag-enabled Board and a non-removed item in a terminal status. It writes tags through the governed tag service as moltaro-system-automation; tag events and Board audit preserve function, run, version, original-user, and correlation metadata. It never reopens or moves the item and does not change Board Data, status, CompletedAtUtc, rank, run, history, or Board item RowVersion. Read tags back with ITagService or the ordinary resource-list Tags API.

Recover identity for a pre-existing Board item

Section titled “Recover identity for a pre-existing Board item”

Use the separate recovery contract only for an Entity Definition target that was already tracked before its historical SourceKey was available:

  1. Call GetHistoricalRecoveryStateAsync with the exact Board, case-sensitive source key, and target.
  2. Use exactly the returned recommendation: AdoptExistingItem for one non-removed unbound pass, or CreateAfterRemovedItem for one removed unbound pass.
  3. Pass the returned BoardItemId and RowVersion unchanged to RecoverHistoricalItemAsync, together with the desired ImportHistoricalBoardItemRequest.
  4. Handle Adopted, Created, Updated, or Unchanged.
  5. After an unknown result, retry the same guarded request or call GetHistoricalImportAsync for the source key.

Adoption preserves the existing Board item and Board Data ids, comments, attachments, responsibilities, links, history, audit, and origin metadata. Recovery after removal leaves the removed pass untouched and creates RunNumber = removed.RunNumber + 1 with PreviousBoardItemId = null. Multiple prior passes are ambiguous. Self-contained and Entitlement targets are not supported by this recovery contract.

The older IBoardRuntimeCommandService and IBoardRuntimeDefinitionProvider are advanced TrustedDirectDb interfaces; they do not promise the complete application/WebApp orchestration above. See the exact C# Boards recipes and inspect the installation’s developer-surface response before injecting a service. A direct package host that enables event Board Constraints must register an IBoardConstraintRuntimeEvaluator through AddMoltaroBoardsConstraintRuntimeEvaluator<TEvaluator>(); the package default fails closed when an enabled Binding applies and no production evaluator is available. That public custom-evaluator contract receives Board event plans; proposed-state status-invariant evaluation is supplied by the Moltaro application host to generated Package and Net Operation Project contexts. A standalone package-only host fails closed for applicable status invariants. Authoritative mutations invoke EvaluateAuthoritativeBatchAsync after taking their governance locks. A cache-backed custom evaluator must override that method and reload its applicable configuration and compiled plans inside the call; delegating to EvaluateBatchAsync is safe only for uncached evaluation sources.

GET /api/workspace/boards (operation) returns the active boards the caller may see:

Terminal window
curl -s https://ops.example.com/api/workspace/boards \
-H "Authorization: Bearer <token>"
{
"Data": [
{
"Id": "zuq5JQx4kLF1",
"Key": "support_triage",
"DisplayName": "Support triage",
"Description": "Incoming ticket triage and resolution.",
"TagsEnabled": false,
"DueDateEnabled": false
}
],
"Success": true
}

Item endpoints accept either the board Id or the Key as {boardIdOrKey}.

GET /api/workspace/boards/{boardIdOrKey}/items/create-context (operation) is the discovery step before creating anything: it lists the board’s target definitions (with the full create-form model per target), the statuses, and what the caller may do:

Terminal window
curl -s https://ops.example.com/api/workspace/boards/support_triage/items/create-context \
-H "Authorization: Bearer <token>"
{
"Data": {
"Board": { "Id": "zuq5JQx4kLF1", "Key": "support_triage", "DisplayName": "Support triage" },
"Statuses": [
{ "Id": "2FEzzRX4vMNg", "DisplayName": "Triage", "MetaType": 0, "SortOrder": 1 },
{ "Id": "x2lpSYpWQ3q6", "DisplayName": "In progress", "MetaType": 1, "SortOrder": 2 },
{ "Id": "gc3NgI61wO4D", "DisplayName": "Resolved", "MetaType": 2, "SortOrder": 3 }
],
"TargetOptions": [
{
"TargetDefinition": {
"Id": "ojmxCpYFwfCY",
"TargetKind": 0,
"TargetModelId": "pYZqCLZWb0K1",
"DisplayNameSingular": "Support ticket",
"CanAdd": true,
"CanMove": true
}
}
]
},
"Success": true
}

An agent should treat this response the way it treats an entity schema: read it first, then act on what it declares.

POST /api/workspace/boards/{boardIdOrKey}/items/create (operation) creates the record and its board item in one atomic call: TargetCreate carries the record fields (same write shapes as record creation), BoardData carries the process context:

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/create \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "BoardTargetDefinitionId": "ojmxCpYFwfCY",
"TargetCreate": { "Fields": {
"Title": "VPN drops every hour", "Priority": "high" } },
"BoardData": { "Subject": "VPN drops every hour" } }'
{
"Data": {
"Id": "1WtfVd0cpu9x",
"BoardId": "zuq5JQx4kLF1",
"Target": {
"BoardTargetDefinitionId": "ojmxCpYFwfCY",
"TargetKind": 0,
"TargetModelId": "pYZqCLZWb0K1",
"TargetObjectId": "89aed6c628e54e2ab4a782ae30fb156d"
},
"BoardStatusId": "2FEzzRX4vMNg",
"IsOpen": true,
"RowVersion": "6f2acd60-ac51-4adb-ac19-49784f243436",
"Actions": { "CanMove": true, "CanReadHistory": true, "CanEditBoardData": true, "CanViewAuditTrail": true },
"Card": { "Number": 1, "Subject": "VPN drops every hour", "BoardStatusDisplayName": "Triage" }
},
"Success": true
}

The new record exists as a normal SupportTicket (Target.TargetObjectId), and the item landed in the board’s initial status. To put an existing record on the board instead, use add-context to preview admission and POST /items to add it.

Position is optional on POST /api/workspace/boards/{boardIdOrKey}/items. When it is omitted, Moltaro appends the new item to the destination status. Explicit boundaries describe the current order in that same board, status, and cycle:

  • PreviousBoardItemId alone inserts after the current last item;
  • NextBoardItemId alone inserts before the current first item;
  • both ids insert between two items that are currently adjacent.

Read the current board order immediately before constructing explicit boundaries. A removed item, an item from another board/status/cycle, a stale edge, or a non-adjacent pair returns HTTP 400 with the stable code moltaroBoards.runtime.validation.positionInvalid; re-read the board and decide the position again. Do not retry the same stale payload unchanged.

{
"TargetKind": 0,
"TargetModelId": "pYZqCLZWb0K1",
"TargetObjectId": "89aed6c628e54e2ab4a782ae30fb156d",
"RepeatMode": 1,
"Position": {
"PreviousBoardItemId": "item-before",
"NextBoardItemId": "item-after"
}
}

The server computes what moves are allowed — the transition graph, permission checks, and process rules all apply. Never hardcode status flows; ask:

Terminal window
curl -s https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/move-options \
-H "Authorization: Bearer <token>"
{
"Data": {
"BoardItemId": "1WtfVd0cpu9x",
"FromBoardStatusId": "2FEzzRX4vMNg",
"Options": [
{ "BoardStatusId": "2FEzzRX4vMNg", "DisplayName": "Triage",
"Allowed": true, "BoardTransitionId": null, "Errors": [] },
{ "BoardStatusId": "x2lpSYpWQ3q6", "DisplayName": "In progress",
"Allowed": true, "BoardTransitionId": "qRO10Ii6gAbn", "Errors": [] },
{ "BoardStatusId": "gc3NgI61wO4D", "DisplayName": "Resolved",
"Allowed": false, "BoardTransitionId": null,
"Errors": [ { "Code": "moltaroBoards.runtime.transitionNotFound",
"Message": "This card cannot be moved to that status." } ] }
]
},
"Success": true
}

Resolved is blocked because the board defines no Triage → Resolved transition. POST .../move/validate (operation) runs the same checks for one concrete move as a dry run — no state change:

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/move/validate \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "ToBoardStatusId": "gc3NgI61wO4D" }'
{
"Data": {
"Allowed": false,
"BoardItemId": "1WtfVd0cpu9x",
"FromBoardStatusId": "2FEzzRX4vMNg",
"ToBoardStatusId": "gc3NgI61wO4D",
"BoardTransitionId": null,
"Errors": [
{ "Code": "moltaroBoards.runtime.transitionNotFound",
"Message": "This card cannot be moved to that status." }
]
},
"Success": true
}

Note the envelope: Success: true because the validation itself succeeded — the verdict lives in Data.Allowed and Data.Errors.

The validation result also returns CapturedAtUtc and ordered ConstraintDecisions when Board Constraints were evaluated. Execute promptly with the same RowVersion. The move command rebuilds the authoritative plan under its lock; preview is not a reservation. For the same row versions and proposed state, preview and execute use the same Business Invariant and Board Constraint preparation path and return the same ordered failures.

The move-options endpoint evaluates all destination candidates as one request-local batch. Board definition lookups, Constraint configuration, compiled plans, and bounded entity dependencies are shared across candidates; the runtime does not repeat full validation once per status.

When a transition has required item links, its move option and validation result include LinkPrerequisites. Each row reports the configured scope, CurrentCount, MissingCount, and Satisfied. Only active links to non-removed items in the exact configured Board target definition count. Missing links return moltaroBoards.runtime.moveLinkPrerequisiteMissing.

The move request can satisfy missing rows with LinkPrerequisiteSelections. Select an existing readable item with OtherBoardItemId, or atomically create a target Board item with CreateItem; supply exactly one per selection:

{
"ToBoardStatusId": "x2lpSYpWQ3q6",
"RowVersion": "6f2acd60-ac51-4adb-ac19-49784f243436",
"LinkPrerequisiteSelections": [
{
"BoardTransitionLinkPrerequisiteId": "required-parent",
"OtherBoardItemId": "parent-board-item-id"
}
]
}

Moltaro derives the link kind and direction from configuration; clients cannot override them. The selected item must match the configured Board and target definition, normal read/link permissions and link invariants still apply, and link creation plus movement commit in one transaction. With CreateItem, the new target, its Board Data, the link, and the move all commit or roll back together.

POST /api/workspace/boards/{boardIdOrKey}/items/{boardItemId}/move (operation) executes the move; send the RowVersion you last saw, exactly like a record update:

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/move \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "ToBoardStatusId": "x2lpSYpWQ3q6",
"RowVersion": "6f2acd60-ac51-4adb-ac19-49784f243436" }'
{
"Data": {
"Id": "1WtfVd0cpu9x",
"BoardStatusId": "x2lpSYpWQ3q6",
"IsOpen": true,
"RowVersion": "738941bd-5d82-4013-b64d-0063012b83c9",
"Card": { "Number": 1, "Subject": "VPN drops every hour", "BoardStatusDisplayName": "In progress" }
},
"Success": true
}

A stale RowVersion fails the move instead of silently overriding a concurrent change — keep the value returned by each mutation.

Search board items through live Entity search targets

Section titled “Search board items through live Entity search targets”

POST /api/workspace/boards/{boardIdOrKey}/items/search combines SearchText with status, cycle, target, Board Data, Target Data, responsibility, tag, due date, and link filters. The filters narrow the Board candidate set; SearchText then matches Board-owned text and identity or the live target projection.

For an Entity target, Moltaro uses that definition’s current configured SearchTargets, including supported direct and related fields, table paths, number, comments, and attachments. The same access-aware Entity search SQL used by record lists is applied in bulk to target ids already present on the Board. There is no Board-side copy of Entity content, synchronization job, item reopen, or Board rebuild. Updating SearchTargets changes the next Board search after the normal definition snapshot refresh.

Entity content contributes a match only when the caller can normally read the root record and every protected linked path involved in that configured target. Restricted text cannot change the returned rows or TotalCount. Each Board target definition is evaluated separately, so equal target ids from different Entity definitions cannot cross-match. If SearchTargets is empty, configured Entity content does not match; stable target identity, readable target presentation, and Board-owned text remain available as compatibility fallbacks. Cursor paging and the other Board filters are unchanged.

Boards does not copy Entity text into Board Data and does not own a separate search index, reconciliation task, or reindex/rebuild operation. Each request reads the current Entity PostgreSQL storage using the same parameterized search/index contract as Data Explorer. Candidate ids are processed in bounded keyset batches; each match batch is written to request-local indexed PostgreSQL temporary tables and released from application memory. Count and page queries join those tables, while Board Data uses keyset batches without repeated count or OFFSET queries. The complete search, including Count and page materialization, has a 30-second deadline. If a target provider fails or any stage times out, search fails closed with moltaroBoards.runtime.searchProviderUnavailable; Moltaro does not return a partial page or partial TotalCount, and diagnostics never include SearchText.

Section titled “Search link candidates within an explicit scope”

POST /api/workspace/boards/{boardIdOrKey}/items/{boardItemId}/link-candidates returns non-removed items that the caller may read and may link from both endpoints. Use BoardTargetDefinitionIds whenever the workflow already knows which target types are valid:

Terminal window
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/link-candidates \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "SearchText": "VPN",
"BoardTargetDefinitionIds": ["ojmxCpYFwfCY"],
"LinkType": 1,
"Direction": 0,
"Limit": 10 }'

BoardTargetDefinitionIds scopes the complete search pipeline, not only the last result filter. Moltaro invokes target search only for the supplied active definitions, searches Board Data only for their owning active Boards, and queries final candidates only from those definitions. An unknown or inactive id fails with the stable moltaroBoards.runtime.targetDefinitionNotFound error. Read access, ManageItemLinks, Business Invariants, duplicate-link rules, and Child or Related cycle rules still apply to every returned candidate; inaccessible items do not disclose target metadata.

Omitting BoardTargetDefinitionIds intentionally keeps workspace-wide search across active Boards and target definitions. Each provider and Board Data presearch is bounded before final candidate materialization. This bounded workspace-wide search returns a limited page; clients must not expect every match to be loaded. The endpoint also supports free text and exact #RunNumber lookup. Caller cancellation and the HTTP request deadline propagate through provider and database work; after an unknown or cancelled response, issue a new bounded search instead of assuming that a background search continues.