Skip to content
Troubleshooting

Fix Business Process and Electronic Approval

Diagnose a Business Process descriptor or Electronic Approval business form that is missing, unrenderable, or refused, without weakening lifecycle or permission checks.

Business Process and Electronic Approval

For a missing descriptor, check discovery, tenant installation, lifecycle, and permissions in order. Rendering and execution failures have separate checks below:

Start with the doctor, which settles the first:

Code example
Shell
task artisan -- nexia-apps:doctor-package-app quality

A composer entry is not proof that a record action is authorized. The descriptor makes an action eligible. Your App still validates the payload and enforces the Policy on the final write.

Symptom index

SymptomSection
InvalidArgumentException at boot naming a descriptorDescriptor fails validation at boot
kind [X] must be one of: serviceTask, sendTask, receiveTaskWrong work-action kind
Action absent from the BPMN pickerDescriptor never appears
Binding absent from the Approval composerDescriptor never appears
Binding offered, composer renders nothingBinding renders nothing
Approval document is empty or mislabeledDocument is empty or mislabeled
Labels show as raw i18n keysLabels show as raw keys
A published process fails at a taskA published process breaks
Approved case did not change the recordApproval succeeded but nothing happened
Bound approval submission is refused as stale or mismatchedBound submission is refused
Existing work uses an old behaviorBehavior changed under a running definition

Descriptor fails validation at boot

ApprovalFormBindingDescriptor [quality.inspection_decision.submit] must declare at least one entry mode.
ApprovalFormBindingDescriptor [quality.inspection_decision.submit] entry mode [attach] must be one of: create, link
ApprovalFormBindingDescriptor [quality] resource_key must be a non-empty string.
SlotWidgetDescriptor [quality.x] family_key must be a non-empty string.
ProcessTemplateDescriptor key must be a non-empty string.

Cause. Descriptor constructors validate eagerly, so a malformed descriptor fails at boot rather than mid-request. The message names the derived descriptor id.

Fix. Read the field named in the message. Common causes:

Message mentionsFix
entry modeUse create, link, or both — nothing else
must be a non-empty stringSupply the value
family_keyPass null to omit, not ''

Whitespace-only values are rejected specifically to catch an accidental empty string.

Confirm. The application boots and the descriptor appears in its registry.

Prevent. Keep matching binding/schema identities together, as in packages/assets/src/Descriptors/AssetApprovalDescriptors.php. Follow Electronic Approval for the integration path.

Wrong work-action kind

ProcessWorkActionDescriptor [quality.inspection.finalize] kind [userTask] must be one of: serviceTask, sendTask, receiveTask

Cause. userTask is deliberately absent — human tasks are a different family.

Fix. Choose by interaction shape:

KindMeanstopic is
serviceTaskThe process calls your App and waitsA worker topic your App consumes
sendTaskThe process emits a messageA message topic
receiveTaskThe process waits for a messageA message topic

For a human task, publish a ProcessUserTaskFormDescriptor in the same AppDescriptorSet.

Confirm. The action appears in the BPMN element picker.

Prevent. A form for a person is never a work action.

Descriptor never appears

Action absent from the BPMN picker, or binding absent from the Approval composer.

Cause. One of the gates below. Check in order — the first failure explains it. Discovery and installation always apply; the other two apply conditionally:

GateCheckSymptom when unmet
Discoverynexia-apps:doctor-package-app → contribution locationsThe class was never scanned
InstallationInstallation state for this tenantThe App is not operational, so nothing resolves
LifecycleThe descriptor's statusDeprecated blocks new selection but the runtime still resolves it for work already published against it; Removed stops resolution
AuthorizationThe actor's permission, where the descriptor declares oneCorrectly hidden from an actor without it. ProcessTemplateDescriptor and ProcessWorkActionDescriptor carry no permission field, so this gate does not apply to them

Fix. For discovery, confirm the class sits under a directory declared by contributionLocations() and that it declares the contribution interface — a trait alone does not make it discoverable. For installation, nexia-apps:repair-tenant-app. For lifecycle, set Active. For authorization, assign a Role carrying the permission.

Confirm. The descriptor appears for an actor holding the permission in a tenant where the App is operational.

Prevent. Check permission and installation before suspecting discovery — the doctor settles discovery in seconds.

Binding renders nothing

Binding offered, composer renders nothing.

Cause. An Approval binding is not self-contained. Three artifacts join by key, and a mismatch in any one produces a binding the composer can offer but not render:

ArtifactKeyMust match
ApprovalFormBindingDescriptorformWidgetKeyA SlotWidgetDescriptor.key
SlotWidgetDescriptorslotapproval.composer.business_form
SlotWidgetDescriptorcomponentA registered frontend component

Fix. Align the keys, then confirm the component is registered:

Code example
Shell
task artisan -- nexia-apps:doctor-package-app quality

The doctor cannot confirm this pairing — its shell component pairing check walks pageElements() only, and a Slot Widget component is not in that map. Confirm the name is bound in resources/js/index.ts yourself. Add the surface on its derivable path, or add an overrides entry, then rebuild:

Code example
Shell
task artisan -- nexia-apps:activate-package-app quality --build-assets

Confirm. Selecting the business form renders your component in the composer.

Prevent. Keep the binding key and widget key aligned; packages/assets/src/Descriptors/AssetApprovalDescriptors.php derives the binding from its Resource/action pair. Add a Slot Widget covers component registration.

Document is empty or mislabeled

Approval document is empty or mislabeled.

Cause. The binding's documentSchemaKey does not match a contributed ApprovalDocumentSchema. The schema is keyed {appKey}.{resourceKey}.

Fix. Confirm the binding's documentSchemaKey equals the schema's {appKey}.{resourceKey}, and that the class returns its ApprovalDocumentSchema in AppDescriptorContribution::appDescriptors(). Ship bindings and schemas together, as in packages/assets/src/Descriptors/AssetApprovalDescriptors.php.

Confirm. The approver sees the declared sections and fields.

Prevent. The schema declares what an approver reads, not what your App stores. Include the fields needed to decide; exclude internal bookkeeping.

Labels show as raw keys

quality.approval.inspection_decision.submit.label

Cause. A labelKey, descriptionKey, approvedLabelKey, or rejectedLabelKey has no entry in the package locale catalogs.

Fix. Add the key to every supported locale — ko, en, zh — as flat JSON with a string value, then clear the cache:

Code example
Shell
task artisan -- nexia-runtime:validate-translations
task artisan -- nexia-runtime:clear-translation-catalog-cache

Confirm. OK translation catalogs, and the label renders in each locale.

Prevent. Add all locales in the same commit as the descriptor.

A published process breaks

A published process fails at a task whose descriptor was removed.

Cause. The Process definition resolves a Work Action by {app, action_key}. Each created external task freezes the resolved App, action, version, and topic. It fails closed when that frozen tuple no longer matches the active descriptor, the descriptor is Removed or deleted, or the App is deactivated. Process Templates are only creation-time sources and are not runtime dependencies of an already-published definition.

Fix. Restore the descriptor to Active, or reactivate the App. If the capability is genuinely retired, the referencing definitions must be re-authored onto a replacement.

Confirm. The process resumes at the failing task.

Prevent. Retire in sequence:

  1. Deprecated — new authoring stops selecting it; running work continues.
  2. Wait for existing references to drain, or migrate them.
  3. Then Removed, or delete.

Deleting outright behaves like Removed, with no record of why.

Approval succeeded but nothing happened

Approved case did not change the record.

Cause. A missing, failed, or rejected App outcome handler may leave the record unchanged. A binding routes a request; it does not authorize a write. The platform records the outcome and notifies your App. Your App then applies the change, enforcing its own Policy.

Fix. Implement and test the record action your App performs on the approved outcome. submitPermissionKey gates submission; it does not gate the eventual write.

Confirm. After approval, the record reflects the change and the Policy was evaluated.

Prevent. An approved case is an input to your App's decision, not a substitute for it. Do not skip the Policy because Approval said yes.

Bound submission is refused

Bound approval submission is refused as stale or mismatched.

Cause. The selected template no longer matches the submitted binding key or version, the current descriptor version differs, the ResourceRef is not canonical for that binding, or the resolved Approval line is stale. The host checks all of this before creating a case.

Fix. Re-resolve the template, binding, canonical Resource reference, Resource version, and Approval line, then build a fresh BoundApprovalSubmission. Do not retry with an edited subset of the old snapshot.

Confirm. ApprovalHost::caseSummary() returns the frozen binding and Resource versions after submission. A missing case or non-canonical legacy reference returns null, not a partial summary.

Prevent. Treat binding, template, Resource, and line versions as one frozen decision. Carry an idempotency key and fingerprint when the operation can replay.

Behavior changed under a running definition

Existing work uses behavior different from when the definition was published.

Cause. The handler changed while its descriptor retained the same version, so an external task's frozen version no longer identifies the original behavior.

Fix. Restore the existing action's behavior. For incompatible behavior, publish a new action key and update definitions deliberately. The Work Action registry resolves by App and action key, not by version; publishing a second version does not preserve a selectable old implementation. Replacing the version under the same key causes existing external tasks to fail their version check.

Confirm. Existing tasks retain their expected effect, and newly authored definitions use the replacement key. Recover failed tasks only after checking whether a business effect already committed.

Prevent. Choose the release strategy before changing a published action:

ChangeEffect on existing external tasks
Change behavior at the same versionThe version check cannot detect semantic drift
Replace the version under the same action keyFrozen tasks fail the descriptor-version check
Add a replacement action key; retain the old handlerExisting tasks can continue through the old key
DeprecatedHidden from new selection; existing runtime resolution remains available
Removed or deleteResolution fails
Source of truth: docs/developers/content/en/troubleshooting/business-process-and-approval.md