Custom App operations
Custom React Apps can read and change platform state through the Ennea SDK without embedding standard business components. Konnie can build the screen sequence, conditional checks, photo requirements, status indicators and calculations in the App, and expose useful parameters in its settings. The platform handles saved records, current permissions, quantities and concurrent changes.
Ask Konnie to load the app-programs, production-station and app-evidence references before building the workflow. Use actual work-center, order, position and holder IDs. Editing saves a draft with history; publication is a separate action.
Develop in the code workspace
Use Quick open (⌘/Ctrl P) to find a file across the App's source programs. Include the program name to distinguish matching paths, or append :line:column to jump to a location. Commands (⌘/Ctrl Shift P) opens search, change review, bindings, formatting and view controls. These shortcuts apply while focus is in the code workspace.
Search in files (⌘/Ctrl Shift F) finds literal text, with optional case and whole-word matching. Each occurrence links to its exact location. Large result sets show the first 100 matches and say when you need to narrow the query. Changes highlights additions and removals against the saved baseline; expand hidden unchanged lines for context or revert one file.
In preview or split view, choose Fit, Mobile, Tablet or Desktop to inspect layout at different widths. Resizing and switching views retain the running preview's state. The controls change viewport width; they do not emulate a device's touch support or browser.
After checking code, open Build insights for file sizes, compiled output size and the local import graph. Follow a dependency to its source. Reports belong to the checked source and disappear when that source changes. Compiled bytes exclude packaged libraries, network transfer and browser work. An unreferenced module is informational; styles may still apply globally. A successful check proves compilation, so also exercise the preview and the intended workflow before publication.
Identical simultaneous native, dataset and Table reads within one running App now share pending work. Each caller receives an independent result, completed results are not cached, and writes or change notifications force subsequent reads to use fresh transport. Continue using scoped SWR keys and authoritative refetch after reconnect.
Let Konnie work with a running App
An App can expose its selected task and named actions with useAppInteraction. For example, a material request can expose its current answers, patchDraft and submitRequest. Konnie can fill the working answers, explain them, or use the same submission handler as the App's button. The App owns the fields, layout, validation and workflow. A separate Form or embedded chat component is unnecessary.
In regular Konnie chat or the drawer, select the running App beside the message input. This connects the message to that App's current task. Other permitted normal-account tools remain available. If the App closes, the task changes, permission is revoked or the account changes, an old response cannot be applied to the replacement task.
For an App-local interface, useKonnie provides messages, send/cancel, a host conversation window, recorded dictation and attachment selection. App authors can design their own controls or use the host window. An App-originated conversation always uses the published App's operational tool policy, including for a normal account. Shop-floor accounts additionally require their assigned App, current device session and relevant resource permissions. A shared account does not identify an individual operator.
Ask Konnie to use the app-interaction-authoring skill when building these features and app-interaction-operations when operating them. Builder topic app-interaction contains the current API. The Konnie-assisted data entry starter is editable source with an optional App-local conversation. To use its existing Table submission flow, connect your Table before submitting.
Show saved status and preserve manual edits. Reversible assistance can be applied directly with an appropriate undo action. A command that was queued or delivered has not yet confirmed a save or submission. Use the returned canonical receipt for consequential operations. If a request is interrupted, reconcile or retry its saved exact intent with the same invocation ID; do not silently repeat an uncertain action. Working drafts need connectivity for saving and submission.
Share records across Apps
Use a shared Model for a new record type. Models can enforce rules for state changes, keep a record history and emit events that Automations can use. For example, several Apps can use one Model for a shift handover that starts at one station and continues at another. Each App has its own screen, settings and permitted actions. Records remain saved independently of the App's layout and publication history.
| Need | Use |
|---|---|
| New shared record type | Model. Use writes: "actions" when state changes follow rules. |
| Extra attribute or link on an existing Ennea record | Custom field or relationship extension |
| Observation, photo or measurement attached to production work | App evidence |
| Existing Custom Table and its Apps | Keep the Custom Table |
Ask Konnie to load the workspace-models builder topic, define the Model's fields and connect the Apps. Define inputs in the App. When the Model requires actions, bind its create action by key in actions and call it on submission. Existing Custom Tables and the Apps that use them remain supported.
For example: “Create a shift-handover Model with a reference, status, notes and checks. Connect a capture App and a review App to the same records. Let reviewers continue existing handovers.” Choose personal records or shared workspace access for each connection. Shared access requires an authorized connection; knowing a record's reference does not grant access.
The App connection names the Model and its structure version and declares permitted operations. React Apps use ennea.models.describe, query and get for reads. Use create and update for direct writes if the Model allows them, or act for a bound action. Fields support text, numbers, booleans, day-only dates, datetimes, choices, nested groups, lists and references to supported Ennea records or other Models. Konnie can add optional fields in a new version; existing App connections keep their selected version until deliberately updated.
Use choice for a fixed set of values. Define options: [{key, label: {en, de}}] with 1–100 distinct keys and both labels. Keys start with an ASCII letter, contain only ASCII letters, digits or underscores, and have at most 64 characters; __proto__, constructor and prototype are reserved. Save the key, such as open, rather than the displayed label. A list whose items has type choice stores a multi-select array of keys. Later versions may add options or change labels, but cannot remove or rename existing keys.
{
"key": "state",
"label": { "en": "State", "de": "Status" },
"type": "choice",
"options": [
{ "key": "open", "label": { "en": "Open", "de": "Offen" } },
{ "key": "done", "label": { "en": "Done", "de": "Erledigt" } }
]
}
Use date for a calendar day (2026-11-08) and datetime for an instant (2026-11-08T15:04:05.123Z). Datetimes require a full ISO 8601 UTC timestamp with seconds and Z or +00:00; optional fractional seconds are normalized to millisecond precision with Z. Convert a local date/time to UTC before saving. Studio displays choice labels in the current language and datetimes in your browser's timezone. Top-level choice and datetime fields can be the Model's displayField and support scalar filters.
Query related records on the server. A single where: {field, value} remains supported and means equality. Use op: 'eq' explicitly or op: 'in' with values containing 1–50 scalar or choice values. Combine 1–4 conditions with {all: [...]}. Filters address top-level fields; references support equality with their exact {objectType, entityId} or {modelId, recordId} value. References inside groups or lists cannot be filtered.
const page = await ennea.models.query("requests", {
where: {
all: [
{
field: "machine",
value: { objectType: "work_center", entityId: selectedMachineId },
},
{ field: "state", op: "in", values: ["open", "pending"] },
],
},
order: { field: "dueAt", direction: "asc" },
limit: 20,
});
order accepts newest (the default), oldest, or a top-level datetime field with direction: 'asc' or 'desc'. Follow nextCursor for more results and keep the same filters and order between pages. Current workspace, personal-record and privacy permissions apply to every page.
Open Operations Studio → Models to browse accessible Models. Select one to see its Records or inspect its Structure. Open a record to inspect its saved values; make operational changes in a connected App. Lists load in pages, with Load more for older records. If another person saved first, reload the current record and review it before saving your changes. This protects shared work; it does not provide simultaneous text editing.
The record view’s Linked records section lists accessible records whose top-level reference points to this record, grouped by Model. Open an item to inspect it and use Load more for further links. Konnie can also use workspaceModels with action: 'linkedRecords' and target: {modelId, recordId} or {objectType, entityId}; results remain limited to readable Models and records. A readable record also stays visible when one of its own links points to something you cannot open; that link shows Linked record not available to you and reveals nothing else.
Open Show history in a record to see its saved changes, newest first: the version, which fields changed, whether a person, a shared shop-floor account, Konnie or an automation made the change, and whether it came from an App or Studio. History begins with the first change saved after this feature became available; earlier changes were not recorded. If a past version linked to a record you can no longer open, the entry stays visible without its values. In a React App, ennea.models.history(alias, recordId, {limit, cursor}) returns the same entries under the App's read connection.
For administrators preparing an existing database, use the explicit index tooling before enabling relationship reads. Point MONGODB_URI at the exact target database, prepare the workspace index group first, then inspect, apply and independently verify the derived reference index:
pnpm --filter @ennea/backend exec node scripts/appRuntimeReleaseIndexes.js --group workspaceReferences --dry-run
pnpm --filter @ennea/backend exec node scripts/appRuntimeReleaseIndexes.js --group workspaceReferences --apply
pnpm --filter @ennea/backend exec node scripts/appRuntimeReleaseIndexes.js --group workspaceReferences --verify
The full pnpm --filter @ennea/backend apps:indexes --dry-run / --apply / --verify release sequence includes both groups in order. The backfill only rebuilds derived relationship entries; it does not change record values, versions, timestamps or history. Missing exact scoped Model definitions and concurrent source changes block completion and require investigation before retrying. Run it once every server runs this release. Until verification passes, reference filters and Linked records report that relationship queries need activation; other Model reads keep working.
Nexus shows Model definitions, their reference relationships and connected Apps in its configuration map. Open a Model there to inspect it in Studio. Individual Model records are currently inspected in Studio or their connected App.
Define actions and rules
Define business operations on the Model so the same rules apply in every connected App, Konnie and Automation. Set writes: "actions" to require actions for creating and changing records. Generic create and update then refuse the write. Existing Models keep their current behavior unless this mode is deliberately enabled.
For returnable racks, use a rack Model with status available, out or damaged, and a rackCirculation Model with a rack reference, status out, overdue or returned, returnedAt (datetime) and returnedBy (actor). Define checkOut to create a circulation only when the linked rack is available, then invoke the rack's dispatch action. Define registerReturn on the circulation as follows:
{
"key": "registerReturn",
"label": { "en": "Register return", "de": "Rückgabe erfassen" },
"kind": "update",
"parameters": [
{
"key": "condition",
"label": { "en": "Condition", "de": "Zustand" },
"type": "choice",
"required": true,
"options": [
{
"key": "available",
"label": { "en": "Ready to use", "de": "Einsatzbereit" }
},
{ "key": "damaged", "label": { "en": "Damaged", "de": "Beschädigt" } }
]
}
],
"rules": [
{
"id": "isOut",
"when": { "field": "status", "in": ["out", "overdue"] },
"message": {
"en": "Only racks that are out can be returned.",
"de": "Nur ausgegebene Gestelle können zurückgegeben werden."
}
}
],
"effects": [
{ "set": "status", "to": { "const": "returned" } },
{ "set": "returnedAt", "to": { "now": true } },
{ "set": "returnedBy", "to": { "actor": true } },
{
"invoke": {
"via": "rack",
"action": "receive",
"parameters": { "condition": { "param": "condition" } }
}
}
],
"allow": { "shopFloor": true, "automations": false },
"konnie": "approval_required"
}
The rack's receive action accepts the same condition choices and sets its status from that parameter. Both changes commit together. A failed rule writes nothing. A rule that needs a parameter remains pending in a preview until the parameter is supplied. Linked records are checked with current permissions and privacy restrictions; an unavailable link never reveals its values.
Connect the receiving App with operations: ["read"] and actions: ["registerReturn"], using the exact Model ID and revision. Each binding can list at most 32 action keys; publication rejects unknown or retired keys. The invoked rack.receive is delegated by the action definition and needs no separate App binding. Current group membership and allow.shopFloor still apply. A shared shop-floor actor value identifies the shared account, not an individual operator.
An own connection also limits linked reads back into the same Model to records owned by the current account. References to other connected Models use their declared read access. An unbound linked Model can be checked by the action without granting generic App reads of its records.
const preview = await ennea.models.availableActions("circulations", record.id);
// Display preview.data.actions and their localized failure.explanation values.
const result = await ennea.models.act("circulations", {
recordId: record.id,
action: "registerReturn",
parameters: { condition: "available" },
expectedVersion: record.version,
invocationId: savedInvocationId,
});
if (!result.data.ok) {
showReasons(result.data.failures.map((failure) => failure.explanation));
}
Save the exact pending input and invocation ID before sending; retry an uncertain outcome with the same ID and unchanged input. For a create action, omit recordId and expectedVersion. Previews explain current availability; execution checks permissions, rules and versions again. Rules use the current Model revision even when an App is pinned to an older revision.
In Studio, Structure shows actions, parameters, rules and permissions. A record shows Available actions and reasons, without running them. Ask Konnie why an action is unavailable; it uses the same preview and explains the returned rule message in your language. Running an action follows the stricter of its Konnie policy and Control Center governance. Before approving a consequential Model revision, review its affected published App bindings. Author actions through Konnie or the API; operational buttons belong in Apps.
Archived records and Model datasets
Archiving preserves a record's identity, values and history. Ordinary reads omit archived records; explicitly including them requires the same read permission. Archived records cannot be changed or newly referenced until restored. Existing references retain their original identity and indicate an archived target. A Model cannot be archived while saved App drafts, retained published App versions, other Models' reference fields, referring records or dataset sources depend on it; the blocking report identifies each dependency.
Konnie uses workspaceModels with archiveRecord or restoreRecord and the exact Model ID, structure version, record ID, current record expectedVersion and stable invocationId. For archiveModel and restoreModel, use the Model's lifecycleVersion as expectedVersion; this is separate from its immutable structure version. Both lifecycle changes require current authority and approval by default. Use includeArchived: true on the corresponding reads to find a restore target; an archive refusal returns exact blockingReferences identifiers. A connected App needs the archive operation in its Model binding to call ennea.models.archive or restore. Retrying an uncertain result uses the same invocation and unchanged input.
Dashboard and App-data configuration can select a Model and its structure version. The dataset connection also needs a matching Model read connection and defaults to My records. Shared workspace access requires an authorized workspace connection. Field and choice labels follow the Model's English/German labels. Dataset queries summarize current state only, excluding archived and privacy-restricted records; they do not reconstruct history. Count and grouping support top-level scalar, choice and reference fields; sum, average, minimum and maximum use number fields. Size or time limits produce an explicit error, never a partial final total. App change hints cause an authorized refetch.
Release preparation also requires the workspaceArchives group after workspace and workspaceReferences:
pnpm --filter @ennea/backend apps:indexes --group workspaceArchives --dry-run
pnpm --filter @ennea/backend apps:indexes --group workspaceArchives --apply
pnpm --filter @ennea/backend apps:indexes --group workspaceArchives --verify
This explicit backfill materializes archive defaults without changing saved values, versions, timestamps or history, then prepares the archive-aware v2 indexes. Keep v1 indexes for explicit inclusion. Quiesce old writers during preparation and cutover; ordinary reads/writes fail closed until the required archive indexes are prepared.
Automate Model actions
In Operations Studio → Automations, choose Model record created, Model record updated or Model action applied, then select the exact Model. The action event can optionally select one action. Conditions can use only choice and boolean state, changed fields, the source and the App ID. Choice values display their translated labels. Wait → Event uses the same Model, action and condition rules, together with its record correlation and deadline.
Add Run Model action, select a Model and an action allowed for Automations, and map the record, expected record version and action parameters from the trigger, fixed values or earlier steps. Choose whether a rule refusal stops the run or follows the Refused branch; connect Applied to the successful path. A refusal returns the rule's translated explanation. The Model kernel checks the current rules and permissions for every attempt. A stop-on-refusal action may be the last step.
For example, define markOverdue on rackCirculation as an update action with these rules and effect:
{
"key": "markOverdue",
"label": { "en": "Mark overdue", "de": "Als überfällig markieren" },
"kind": "update",
"parameters": [],
"rules": [
{
"id": "isOut",
"when": { "field": "status", "eq": "out" },
"message": {
"en": "Only circulations that are out can become overdue.",
"de": "Nur ausgegebene Umläufe können überfällig werden."
}
},
{
"id": "pastDue",
"when": { "field": "dueAt", "lt": { "now": true } },
"message": {
"en": "The due date has not passed.",
"de": "Der Fälligkeitstermin ist noch nicht überschritten."
}
}
],
"effects": [{ "set": "status", "to": { "const": "overdue" } }],
"allow": { "automations": true, "shopFloor": false },
"konnie": "unavailable"
}
Use a daily Schedule and Run Model action on matching records. Select the circulation Model and markOverdue, add Status equals Out, choose Due at: earliest first, and set a maximum of 50 records. Records are processed in stable order; each record has its own retry identity. A run reports processed, refused and failed records, plus whether more matches remain. Not-yet-due records are refused by the rule. A record edited after selection is counted as failed for that run; other records continue and the next schedule can select its current version. A later run no longer selects changed records because their status is now overdue; retrying the same run reuses its receipts rather than applying the action twice.
Publication pins the Model revision and checks the action and scoped service identity. Review and republish when the execution rules, effects, permissions or parameter contract changes. Changing labels or rule explanations alone does not require republication. Each execution uses the Automation's service identity, and history records that software actor and the run ID. Removing its Model write permission prevents later execution. Model triggers and waits require both the creator and service identity to retain Model read permission. Without it, each later trigger appears in run history as blocked for missing permission, without event data, and waiting runs are blocked. A trigger's record and version can only target an action on the same Model. Changes originating from Automations are ignored by Model triggers by default; explicitly enabling them still keeps the causation chain bounded.
Model events carry no text, dates, references or actor values. Model action results contain identifiers and change summaries, without record values. These details are withheld from AI-provider nodes; record values require a separately governed read. Ask Konnie to load automations and the shared Model references before building the same workflow. Publication, activation and execution remain separate steps.
Save evidence in the context of the work
An evidence record can contain structured JSON, text and uploaded media. Link it to a customer order, customer-order position, production order, operation, execution split or material holder. For example, the App can save the answers and question labels for an inspection, photographs of a package or calculated packaging dimensions. The App decides which fields apply and what must be saved before its completion action is available.
The SDK provides ennea.evidence.list, get, create, update and delete. Every call contains an explicit subject {type,id}. IDs identify saved records; order numbers and invented package IDs are not subject IDs. An App-specific package reference can be stored inside an order/position's evidence data.
const result = await ennea.evidence.create({
subject: { type: "customerOrderLine", id: selectedPositionId },
kind: "inspection",
data: { checks: [{ id: "visual", checked: true }], packageReference: "A" },
invocationId: savedInvocationId,
});
Use the saved record's revision as expectedRevision when updating or deleting. Deletion removes the evidence record and its associations; it does not automatically erase shared uploaded media. Current subject and file permissions apply when reading saved evidence in another authorized App or at another station.
For evidence required by a native production operation, use the actual productionOrder subject and save its exact data.operationId and data.splitId. Another operation's observation does not satisfy that operation's requirement. A measurement needs a finite numeric data.value and a nonempty unit; preserve its actual label and unit. Authorized Apps and the standard order details reopen the same canonical evidence and files.
Upload and display media
For a resumable upload, save its stable invocationId before dispatch. Call ennea.files.uploadStatus({invocationId}): data.state is ready, pending or missing. A ready result supplies data.file, so recover that canonical file without asking for another selection. For pending or missing uploads, let the operator select or reselect the actual file through ennea.files.select, then call ennea.files.upload(handle, {fileType: 'image', invocationId}) with the same ID. The handle comes from the trusted picker; Konnie cannot invent or choose a local file for the operator.
Retry with the same bytes, file type and immutable App context. Different bytes or context reject instead of replacing the original upload. Save the returned data.file.id in the recovery draft before linking {runtimeFileId} in evidence media. Keep that evidence request's exact invocation and input on uncertain retries. Checkpoint the upload phase before sending bytes: selection while known unsent, uploading before calling files.upload, and ready with its file ID. Cancellation clears only a known-unsent selection whose current status is missing. After possible dispatch, a pending status, or a legacy intent without a phase, cancellation retains the same recovery ID and the outcome remains unknown. Check that the interaction is current after status lookup and after selection, before uploading. Legacy files.create remains available for existing specialized integrations. Selecting or uploading a photograph is separate from saving its evidence association.
Read linked media with ennea.files.read({evidenceId,evidenceSubject:subject,runtimeFileId}). This returns authorized bytes and file metadata for the App's preview. It checks the current record, subject and file rather than exposing a reusable storage URL. A removed/restricted record or lost permission cannot grant continued access through that evidence link.
An owned new upload can be linked for the first time; reuse on the same subject retains current subject and file checks. To link an already operationally used file to a different subject, include its current authorized sourceEvidenceId and sourceEvidenceSubject with that media reference. A file ID, filename or upload origin alone grants no access. Reads of operationally linked files need the evidence/subject context or a validated native order/station selector.
The App can collect one photo or several, before or after a checklist, at position or order scope. These choices are App behavior. Upload limits are technical bounds, not manufacturing requirements.
Reuse media viewers in a React App
Use components.MediaViewer for one image, video, uploaded audio file, PDF or GLB/glTF model, and components.MediaGallery for an ordered mixed collection. These shared viewers accept ordinary React props. The App owns layout, selection and conditions; no embedded business-component configuration is required.
import React from "react";
import { components, useAppSettings, type AppMediaItem } from "ennea";
const { MediaGallery } = components;
export default function App() {
const settings = useAppSettings();
const items = Array.isArray(settings.instructions)
? (settings.instructions as unknown as AppMediaItem[])
: [];
return <MediaGallery items={items} />;
}
Declare the instructions list in the App's settings schema. Each saved item has a stable id, mediaType (image, video, audio, pdf or model3d) and fileId, with optional label/caption and visibility rules. Publication validates and retains these structured file references. A file ID appearing only in React source is not a published asset declaration. Keep files in managed storage, rather than inserting file bytes or storage URLs into source.
For a single file, use <MediaViewer mediaType="pdf" file={reference} label="Assembly manual" />. References can also contain the complete existing evidence, order-attachment or maintenance context. Current App assignment, publication, file and subject permissions apply to every load. Losing access, removing the file or restricting it blocks subsequent reads.
ennea.files.open(reference, {signal}) loads up to 250 MiB using bounded, revision-pinned chunks and returns data.blob, name, mimeType and size. A video reference with delivery: 'playback' selects its compatible playback copy. Shared viewers use this path automatically, show processing/retry states and release temporary Blob URLs when the file changes. Playback begins after the file has loaded; this is not progressive streaming. Ordinary files.read remains available for bounded byte reads, including the existing evidence API.
PDFs provide page navigation, zoom, selectable text and text search where the source contains text. One page is rendered at a time. Browser codecs determine audio/video playback. Use self-contained GLB models for portable 3D instructions; external texture/model URLs cannot be loaded, and CAD formats such as STEP or STL require conversion before preview. Test actual files in the published App as its intended shop-floor account and device, in both languages and at narrow widths.
Files and photos on Model records
A Model file field stores {runtimeFileId}. Each Model file is limited to 64 MiB. Set accept: ["image"] for a photo, or use {type: "list", items: {type: "file", accept: ["image"]}} for a photo series. Accepted kinds are image, pdf, video, audio and model3d; omit accept for all kinds. Later revisions may widen this set. Files are not query filters, display fields, grouping fields or event state.
For a rack return, add a returnConditionPhoto field and a returnPhoto parameter to registerReturn, both {type: "file", accept: ["image"]}. Its effect is {set: "returnConditionPhoto", to: {param: "returnPhoto"}}. Select and upload the actual photo with the trusted picker, retain its upload invocation and returned file ID, then call the action with parameters: {returnPhoto: {runtimeFileId: fileId}}. Retain the exact action input and invocation for uncertain retries. Konnie cannot select or upload a file; it can keep or clear a value or use a file the user already has authority to access.
Preview a saved photo with ennea.files.open({runtimeFileId, recordContext: {modelId, recordId, field: "returnConditionPhoto"}}) or pass that reference to MediaViewer / a MediaGallery item. files.read accepts the same context. For a historical snapshot add its recordVersion. Each call checks the current record's scope, own/workspace access, privacy restriction and file authority. Restricted or erased records stop granting access; an erased file leaves its record valid and shows as unavailable. Studio offers previews on the record and its available history entries.
A new upload must belong to the same actor in the exact scope. Reusing an existing file needs a currently authorized source, supplied separately from values as fileContexts: [{runtimeFileId, recordContext: {modelId, recordId, field}}], or {runtimeFileId, sourceEvidenceId, sourceEvidenceSubject} for evidence. This applies to create/update, action parameters/effects and invoked actions. An ID alone grants nothing. Konnie's workspaceModels readFile reads governed metadata only.
Generate and retain a production record
Bind a published document template through globalConfig.reportTemplates, or an inline structured layout through globalConfig.reportDefinitions. Use separate localized EN/DE aliases and definitions when the cover and layout labels differ, and select the alias using the current App locale. Cooperating Apps reuse an intentionally shared definition identity and version; an unrelated workflow needs its own identity.
Call ennea.reports.describe(alias) and checkpoint its actual binding before reports.create: use expectedDefinitionId, expectedDefinitionVersion and expectedDefinitionDigest for an inline definition, or expectedTemplateId and expectedTemplateVersion for a published template. Save the exact title, orderId, authored workingContent and invocation with these pins. Never invent a template identity or silently adopt a changed binding during recovery.
When copying saved observations into a production-order report, set requireSourceEvidence: true on its inline definition binding and pass sourceEvidence: [{id, revision}] to reports.create. Include every copied canonical production-order evidence record, using its current revision, with at most 100 unique references. Pass an explicit empty array when no generic evidence was copied. Creation and exact creation retries check current source revisions, authority and privacy. Generic authored content remains supported; standalone reports cannot reference order evidence. IDs written into arbitrary text or JSON do not establish this relationship.
Review the report, then explicitly call reports.finalize({template, reportId, version}) with its current version. Checkpoint the actual final version before reports.savePdf({template, reportId, version, invocationId}). This saves the immutable final PDF as a canonical runtime file; checkpoint data.file.id, then link it through evidence media. Resume each phase with its original exact input and invocation after an unknown outcome. A PDF save does not complete production work.
The saved report and PDF retain typed source relationships. Ordinary later evidence edits do not rewrite a historical report, while source restriction, erasure, deletion or lost source authority can prevent further reads. Reopen through the current authorized report/evidence/file APIs; do not duplicate the bytes or records in a separate App store.
The trusted download dialog returns keyboard focus to the originating control when its App remains active and the control is still available. Hiding, replacing or closing the App prevents a late download result from moving focus into the old runtime.
Work with material holders
Use ennea.materialFlow.overview to read holders, material objects and assignments. execute supports creating an output object, placing it, assigning available quantities, collecting/releasing assignments, transferring quantities, correcting an assignment and clearing an explicit selection. A position can span multiple carts; a cart can hold quantities from several positions where its configuration allows mixed contents.
Every movement uses an explicit selection, an operation identity and the current revisions where required. Display the source, destination and quantity before confirmation. Moving or collecting material does not report production progress. Corrections and clearing require their own declared capabilities. Konnie can use selection, scanning or another appropriate interaction without introducing a new platform component.
Save absolute produced quantity before assigning physical output. Active allocations cannot exceed reported output, and reported progress cannot be reduced below active allocations. Transfer or release only the requested partial quantity from the exact assignment and revision, preserving unrelated mixed contents. Collection keeps the original output identity and makes its units available for reassignment. Use the holder's actual positions and matching units; an order reference alone does not identify a physical output lot.
In the production-progress App, movement controls wait while evidence and holders refresh. The entered destination, position and quantity remain available after the refresh completes.
Report progress and station completion
Use ennea.productionStation.write('reportProgress', ...) for absolute split progress: send desired quantityProduced with the observed expectedQuantity and expectedQuantityProduced. Use completePosition for atomic completion of the exact displayed operations, and finishOrder for the station's explicit final confirmation. Keep the observed plan and revisions in the request. The server rejects changed observations instead of overwriting concurrent work. Legacy delta counters and raw order, operation or split progress/status edits cannot bypass the guarded native path. A full reported quantity is distinct from explicit completion.
Read queue, order and history data through productionStation.read. The App chooses which other steps to show, how their completion is represented and which actions follow. Packaging preparation can save a calculation and mark its work done. Pickup can save optional media and report/confirm progress; collecting material from a holder is a separate holder operation.
Enforce published station requirements
Publish root enforceChecklistCompletion: true to enforce a nonempty set of required checklistItems, requirePhoto, requiredEvidenceKinds or requireHolder at the bound work center. All active published policies at that station combine as a union within the exact tenant, site and system. The union applies through independent Apps and native completion controls. Editing a draft does not activate a rule; activating, replacing or removing governing requirements additionally needs work_center.manage authority.
Read the current operation.completionRequirements. Save explicit checklist answers using setChecklist with the observed revision and, when addressing another governing policy, its exact returned completionPolicy: {appId, appVersion, settingsHash}. Never infer a passed check. Required evidence must match the exact order, operation and split. Required holder coverage must equal max(planned, produced) in the matching unit across eligible existing holders; unrelated mixed contents do not count. Save full produced progress before final allocation and completion. Existing closed history is not automatically migrated; retries resolve prior receipts rather than creating new work.
For a growing station queue, use ennea.productionStationPages.read('queueV2', {workCenterId, status, limit:20, cursor, boundedRead:true}), then load only the selected split with read('operationV1', {workCenterId, splitId, includeContext:true}). Its data.context contains the existing operation detail, saved checklist evidence and current completion requirements. Omitting includeContext keeps the original operationV1 response. Existing published Apps keep their saved source and read contracts.
The production-progress App reads shared order evidence in explicit pages of 20 through evidence.list({subject:{type:"productionOrder",id}, operation:{operationId,splitId}, limit:20, cursor}). The optional exact-operation hint bounds authorization to the selected owning graph and preserves shared order evidence; it does not filter evidence to that split. It accepts no kind filter, and unavailable or restricted selected rows fail closed. Existing list calls without the hint keep their original contract. Its holder read uses materialFlow.overview({readMode:'operationV1', productionOrderId, operationId, executionSplitId, holderAfter, selectedHolderId}): this returns output and assignments for that exact split, retained unassigned output identities, assigned holders and a page of candidate holders. Optional selectedHolderId freshly reloads the selected destination outside the page under current permission and privacy checks, preserving the selection, position and quantity on refresh. Follow nextHolderCursor with More holders; an omitted candidate is not an empty holder. quantitiesComplete covers the selected operation only, while occupancyComplete:false makes no claim about other contents. The selected material read allows up to 50 output objects and 100 active assignments, with 20 candidate holders per page plus assigned holders and the selected destination. Larger selections fail instead of displaying partial quantities. The named operation-read indexes must be verified/applied through the existing index procedures before activation; missing indexes fail closed. Writes still perform their current permission, revision, quantity and completion checks.
The explicit bounded read modes also bound privacy-restriction processing across the request, including authorization. They never authorize from an incomplete restriction snapshot; exceeding a supported ceiling or missing a required index fails the read. Existing calls without these opt-ins keep their previous behavior. A PDF explicitly shared from another order retains its independent source-order authorization and privacy checks; being visible on the selected order does not grant access to its source.
Recover without duplicate work
Save the exact pending input and stable invocation/idempotency ID in actor-private App storage before dispatch. Retry an uncertain write with the same ID and identical input. A timeout does not prove that the operation failed. Evidence writes return a saved receipt/revision; a conflicting revision requires a reload and a new informed save.
With useAppDraft, setting a value starts a debounced save. Before a consequential write, include its exact input, invocation ID and destination/version pins in the draft, then await draft.save() and continue only when it returns true. A failed checkpoint must send no write. Keep the same intent for retry, and recheck that the account and active task are still current after waiting.
When another window has saved a different draft, keep both versions for explicit operator review. Use draft.reconcile((local, saved, attempted) => mergedJson) with a pure merge that retains the full pending inputs, target/version pins and every uncertain invocation variant, including an older attempted checkpoint. The callback receives isolated JSON values; it must not send domain actions. Reconciliation freezes draft writes and conditionally saves the merged value against the current storage revision. Continue only after it returns true; a merge error, queue limit or another storage conflict must retain recovery data and block dispatch. Never use a blind reload to discard an uncertain action. Check typeof draft.reconcile === "function" on older runtimes. If unavailable, stop review and keep the original App and version open. Do not suggest reloading, upgrading or changing versions while an unconfirmed private checkpoint is being retained.
Only a successfully confirmed save or reconcile persists the value in canonical actor-private storage. Unconfirmed or conflicting local input remains ephemeral within the same open host and runtime identity, including App version and execution context. A full browser/host reload, crash, sign-out, session expiry or version change does not preserve that private memory. The native beforeunload warning is not durable storage.
The production-progress example retains up to eight distinct action, quantity/selection and evidence-edit variants per recovery group, including the active variant. It refuses overflow instead of dropping work. The operator explicitly loads the saved workspace, reviews the original order/split and then retries the same key through the normal guarded handler. Konnie hands off conflicting draft choices to that operator review; it may use the existing named retry after review, and must report waiting for review rather than completion. Keep only bounded review flags and retained counts in the assistant snapshot, not archived inputs.
Subscribe to change/reconnect events and refetch under current permission. Preserve the selected order, open position and unsaved answers while refreshing. Test desktop and narrow screens in English and German, then test actual saved evidence, media, cart movements and progress with fictional records in the intended environment. A compiled draft or office preview alone does not prove operator access.
A published station policy can additionally require requireFinalPhoto: true. This is a separate customer-order handover requirement, not an operation photo. Read handoverRequirements from the current order, save evidence with the returned evidenceKind, subject {type:"customerOrder",id:order._id} and data:{executionHash:order.executionHash}, then refresh before finishOrder. The backend verifies the current policy union, image admission, source/privacy authority and evidence revision transactionally. Reopening or changing an execution invalidates its earlier final proof even when completion timestamps are unchanged. Handover preserves the accepted evidence snapshot, adds no produced quantity and does not globally complete the customer order.