Examples
Contribute to Business Process
Publish a Workshop action to the BPMN Service Task catalog, implement its App-owned handler, and verify that both sides are registered.
Example type Recipe
Overview
Contribute to Business Process
This recipe exposes an App-owned action that archives a Workshop Note to a
Service Task in Nexia Business Process, then pairs it with the handler invoked
by the Process runtime. It assumes the Workshop Note exists from the earlier
Generate a Resource recipe and that
Create a Contributor added the
workshop.note.archive permission.
What you build
| Component | Responsibility | Observable result |
|---|---|---|
ProcessWorkActionDescriptor | Publishes workshop.note.archive to the BPMN action catalog | Archive note is selectable for a Service Task |
NotePolicy::archive() | Evaluates App archive authority and record scope | The initiating actor is reauthorized through an ordinary App Policy |
ArchiveNoteProcessWorkActionHandler | Archives the Note inside the restored actor and Legal Entity context | Note status changes to archived |
ProcessWorkActionRegistrar registration | Pairs the descriptor identity with a handler factory | The external task completes without an incident |
ProcessWorkActionConformance test | Checks that every Workshop serviceTask has a handler | A missing pairing fails before deployment |

This is the actual editor after applying the Work Action descriptor and locale
entry and refreshing the runtime catalog. Open Automatic task → Action to
run to find Other apps → Workshop → Archive note. Its presence proves
that Core discovered the static descriptor; it does not prove that the handler
is registered or that a Note can be archived. Steps 2, 3, and 6 complete and
verify the executable pairing. No fields in the right pane means the action
has no author-entered payloadSchema; outputs such as status from its
outputContract remain available for the output mapping in step 5.
BPMN Start Event
→ Service Task: workshop.note.archive
→ Core restores the Process Tenant, Legal Entity, and initiating actor
→ the Workshop handler authorizes and archives the Note
→ status = archived output
→ End Event
This recipe supplies the App-owned action consumed by BPMN. It does not create
a Process definition or implement an App-owned Process start. The executing
Process instance must be anchored to a workshop.note Resource. Core and the
handler fail closed when the instance has no Resource or is anchored to another
App's Resource.
1. Publish the Work Action descriptor
Copy the descriptor contributor into Workshop's existing discovery location.
cp docs/developers/examples/business-process-contribution/WorkshopProcessActions.php \
packages/workshop/src/Descriptors/WorkshopProcessActions.phpThe central declaration is:
new ProcessWorkActionDescriptor(
kind: 'serviceTask',
appKey: 'workshop',
actionKey: 'note.archive',
labelKey: 'workshop.process_actions.note.archive.label',
topic: 'workshop.note.archive',
outputContract: [
'note_public_id' => ['type' => 'string'],
'status' => [
'type' => 'string',
'enum' => NoteStatus::values(),
],
],
)kind: 'serviceTask' means the Process waits for the App action to finish.
actionKey is the permanent identity inside the App, while topic is the
worker-routing identity. A BPMN author selects the localized catalog action
instead of typing that topic. Only fields in outputContract can be mapped to
later Process variables through Activity IO.
The descriptor validator can pass after this step, but the action cannot run yet. A descriptor publishes a catalog entry; it never registers a handler by itself.
2. Authorize and implement the App handler
First add the archive ability to
packages/workshop/src/Policies/NotePolicy.php:
public function archive(Actor $user, Note $note): bool
{
return $this->allowsPermission($user, 'workshop.note.archive')
&& $this->matchesAuthorizationContract($note);
}Both the Legal Entity-scoped workshop.note.archive permission and the Note's
record-ownership scope must match. Starting from a Process grants no additional
authority.
Copy the handler into App source.
mkdir -p packages/workshop/src/Process
cp docs/developers/examples/business-process-contribution/ArchiveNoteProcessWorkActionHandler.php \
packages/workshop/src/Process/ArchiveNoteProcessWorkActionHandler.phpThe handler receives the Core-restored context in
ProcessWorkActionInvocation and changes only App-owned state. The complete
example enforces these safety properties:
- It first checks that the
ResourceRefidentifiesworkshop.note. - It finds and locks the Note only inside the current Legal Entity.
- It passes the initiating actor to
Gate::forUser()and reusesNotePolicy::archive(). - It returns the same success for an already archived Note, making retries idempotent.
- It gives recoverable failures stable
ProcessWorkActionExceptioncodes.
On success it returns the descriptor's declared output shape:
return new ProcessWorkActionResult(output: [
'note_public_id' => (string) $note->public_id,
'status' => NoteStatus::Archived->value,
]);The App never imports or updates Core Process instance, token, or external-task models. After receiving the handler result, Core owns external-task completion and token advancement.
3. Register the handler in the service provider
Add these imports to packages/workshop/src/WorkshopServiceProvider.php:
use Amuzcorp\Nexia\Workshop\Descriptors\WorkshopProcessActions;
use Amuzcorp\Nexia\Workshop\Process\ArchiveNoteProcessWorkActionHandler;
use Nexia\Process\Contracts\ProcessWorkActionRegistrar;Register the handler factory from boot() after registering the App:
$this->app->make(ProcessWorkActionRegistrar::class)->register(
'workshop',
WorkshopProcessActions::ARCHIVE_NOTE,
static fn (): ArchiveNoteProcessWorkActionHandler => app(
ArchiveNoteProcessWorkActionHandler::class,
),
);The appKey and actionKey pair must exactly match the descriptor. A matching
topic alone does not create the pairing. After this step, a running Core worker
can route the workshop.note.archive external task to the Workshop handler.
The registrar resolves that exact appKey/actionKey pair; it does not select
a handler by descriptor version or topic.
4. Add the localized label
Add the same key to every resources/lang/{locale}.json supported by Workshop.
The English entry is:
{
"workshop.process_actions.note.archive.label": "Archive note"
}Use a natural label such as 노트 보관 in Korean. When the key is absent, the
BPMN picker may fall back to a generic business label rather than expose a raw
internal key.
5. Connect it to a BPMN Service Task
Refresh runtime caches, then use the Business Process definition editor:
- Connect a Start Event, Service Task, and End Event.
- Select Workshop's Archive note Work Action on the Service Task.
- Map the
statusoutput to a Process variable namedarchive_status. - Save and publish the definition.
- Start a Process instance anchored to a
workshop.note. The initiating actor must haveworkshop.note.archiveauthority and record scope for that Note.
After execution, the Note status and the archive_status Process variable are
both archived, and the instance reaches the End Event. The Work Action
version and topic are frozen when Core creates the external task. Do not change
the meaning of published action key note.archive; publish a new action key
for a semantic change, and retain the existing descriptor version and handler
until its waiting tasks have completed.
6. Verify the registration contract
Copy the contract test into the Workshop package and run the recipe verifier.
cp docs/developers/examples/business-process-contribution/WorkshopProcessWorkActionTest.php \
packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php
sh docs/developers/examples/business-process-contribution/verify.shThe validator must end with App descriptor contracts are valid. and the
package doctor must report no FAIL. The focused package test checks that
every catalogued Workshop serviceTask has a registered handler. The script
ends with:
Workshop Business Process work action is catalogued and paired with a registered handler.
Common errors
| Symptom | Cause | Fix | Confirm |
|---|---|---|---|
| Archive note is absent from the BPMN picker | The descriptor is undiscovered, the App is not installed, or runtime cache is stale | Check the discovery location and installation, then refresh runtime caches | Archive note appears under Other apps → Workshop in the picker |
| The test reports “no handler is registered” | Service-provider registration is missing or its key does not match | Register workshop with WorkshopProcessActions::ARCHIVE_NOTE | verify.sh prints the registration-conformance completion message |
| The instance stops on an external-task incident | The instance has no Resource or is anchored to another App | Start it with a canonical workshop.note ResourceRef | The Service Task completes and the instance reaches the End Event |
| The handler denies the operation | The initiating actor lacks Note archive authority or record scope | Check the Role and Access Grant in the current Legal Entity | The same actor archives the target Note and its status becomes archived |
| The Note is archived but a later Gateway sees no value | Activity IO has no output mapping | Map status to a Process variable such as archive_status | The completed instance contains archive_status=archived |
Next
To start the Process from App code, continue with the App-owned Process start in
Business Process and the
ProcessStarter and BoundProcessStart contracts in
Business Process contracts.
For human input, do not add a userTask kind to the Work Action; publish a
ProcessUserTaskFormDescriptor instead.
Verify behavior across settings
This Workshop example verifies a minimal contribution. It does not automatically add configurable approval requirements or default procedure installation. Read Electronic Approval for the developer's enforcement obligations and the administrator's form, line, and requirement setup. Then follow the real purchase flow in Business Process to compare automatic inputs, human review, required versus optional approval, line preparation, outcome branches, and supplied-template updates in one journey.
Included files
BPMN Work Action descriptor
docs/developers/examples/business-process-contribution/WorkshopProcessActions.php<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\Workshop\Descriptors;
use Amuzcorp\Nexia\Workshop\Enums\NoteStatus;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\ProcessWorkActionDescriptor;
final class WorkshopProcessActions implements AppDescriptorContribution
{
public const ARCHIVE_NOTE = 'note.archive';
public static function appDescriptors(): AppDescriptorSet
{
return AppDescriptorSet::of(
new ProcessWorkActionDescriptor(
kind: 'serviceTask',
appKey: 'workshop',
actionKey: self::ARCHIVE_NOTE,
labelKey: 'workshop.process_actions.note.archive.label',
topic: 'workshop.note.archive',
outputContract: [
'note_public_id' => ['type' => 'string'],
'status' => [
'type' => 'string',
'enum' => NoteStatus::values(),
],
],
),
);
}
}App-owned Work Action handler
docs/developers/examples/business-process-contribution/ArchiveNoteProcessWorkActionHandler.php<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\Workshop\Process;
use Amuzcorp\Nexia\Workshop\Enums\NoteStatus;
use Amuzcorp\Nexia\Workshop\Models\Note;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Nexia\Process\Contracts\ProcessWorkActionHandler;
use Nexia\Process\Domain\ProcessWorkActionException;
use Nexia\Process\ProcessWorkActionInvocation;
use Nexia\Process\ProcessWorkActionResult;
final readonly class ArchiveNoteProcessWorkActionHandler implements ProcessWorkActionHandler
{
public function handle(ProcessWorkActionInvocation $invocation): ProcessWorkActionResult
{
if ($invocation->resourceRef->appKey !== 'workshop'
|| $invocation->resourceRef->resourceKey !== 'workshop.note') {
throw new ProcessWorkActionException(
'The Process instance is not anchored to a Workshop note.',
'workshop_note_resource_mismatch',
);
}
return DB::transaction(function () use ($invocation): ProcessWorkActionResult {
$note = Note::query()
->where('public_id', $invocation->resourceRef->resourceId)
->where('legal_entity_id', $invocation->legalEntity->key())
->lockForUpdate()
->first();
if (! $note instanceof Note) {
throw new ProcessWorkActionException(
'The Workshop note is not available in this Legal Entity.',
'workshop_note_unavailable',
);
}
Gate::forUser($invocation->actor)->authorize('archive', $note);
if ($note->status !== NoteStatus::Archived) {
$note->forceFill(['status' => NoteStatus::Archived])->save();
}
return new ProcessWorkActionResult(output: [
'note_public_id' => (string) $note->public_id,
'status' => NoteStatus::Archived->value,
]);
});
}
}Descriptor and handler conformance test
docs/developers/examples/business-process-contribution/WorkshopProcessWorkActionTest.php<?php
declare(strict_types=1);
use Amuzcorp\Nexia\Workshop\Descriptors\WorkshopProcessActions;
use Nexia\AppDescriptors\ProcessWorkActionDescriptor;
use Nexia\Process\Contracts\ProcessWorkActionRegistrar;
use Nexia\Testing\ProcessWorkActionConformance;
test('every Workshop Process service task has a registered handler', function (): void {
ProcessWorkActionConformance::assert(
'workshop',
WorkshopProcessActions::appDescriptors()->ofType(ProcessWorkActionDescriptor::class),
app(ProcessWorkActionRegistrar::class),
);
});Refresh and verify the Business Process contribution
docs/developers/examples/business-process-contribution/verify.sh#!/usr/bin/env sh
set -eu
for required in \
packages/workshop/src/Descriptors/WorkshopProcessActions.php \
packages/workshop/src/Process/ArchiveNoteProcessWorkActionHandler.php \
packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php
do
test -f "$required" || {
printf 'Missing Workshop Process source: %s\n' "$required" >&2
exit 1
}
done
grep -Fq 'WorkshopProcessActions::ARCHIVE_NOTE' \
packages/workshop/src/WorkshopServiceProvider.php || {
printf '%s\n' 'WorkshopServiceProvider has not registered the Process work-action handler.' >&2
exit 1
}
grep -Fq 'function archive' packages/workshop/src/Policies/NotePolicy.php || {
printf '%s\n' 'NotePolicy does not authorize the archive business action.' >&2
exit 1
}
task artisan -- nexia-runtime:refresh-runtime-caches --if-route-cached --no-ansi
task artisan -- nexia-apps:validate-app-descriptors workshop --no-ansi
task artisan -- nexia-apps:doctor-package-app workshop --no-ansi
task test -- packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php --compact
printf '%s\n' 'Workshop Business Process work action is catalogued and paired with a registered handler.'