Skip to content

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

ComponentResponsibilityObservable result
ProcessWorkActionDescriptorPublishes workshop.note.archive to the BPMN action catalogArchive note is selectable for a Service Task
NotePolicy::archive()Evaluates App archive authority and record scopeThe initiating actor is reauthorized through an ordinary App Policy
ArchiveNoteProcessWorkActionHandlerArchives the Note inside the restored actor and Legal Entity contextNote status changes to archived
ProcessWorkActionRegistrar registrationPairs the descriptor identity with a handler factoryThe external task completes without an incident
ProcessWorkActionConformance testChecks that every Workshop serviceTask has a handlerA missing pairing fails before deployment

Workshop Archive note in the BPMN automatic-task action picker

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.

Code example
Shell
cp docs/developers/examples/business-process-contribution/WorkshopProcessActions.php \
  packages/workshop/src/Descriptors/WorkshopProcessActions.php

The central declaration is:

Code example
PHP
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:

Code example
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.

Code example
Shell
mkdir -p packages/workshop/src/Process
cp docs/developers/examples/business-process-contribution/ArchiveNoteProcessWorkActionHandler.php \
  packages/workshop/src/Process/ArchiveNoteProcessWorkActionHandler.php

The 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 ResourceRef identifies workshop.note.
  • It finds and locks the Note only inside the current Legal Entity.
  • It passes the initiating actor to Gate::forUser() and reuses NotePolicy::archive().
  • It returns the same success for an already archived Note, making retries idempotent.
  • It gives recoverable failures stable ProcessWorkActionException codes.

On success it returns the descriptor's declared output shape:

Code example
PHP
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:

Code example
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:

Code example
PHP
$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:

Code example
JSON
{
  "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:

  1. Connect a Start Event, Service Task, and End Event.
  2. Select Workshop's Archive note Work Action on the Service Task.
  3. Map the status output to a Process variable named archive_status.
  4. Save and publish the definition.
  5. Start a Process instance anchored to a workshop.note. The initiating actor must have workshop.note.archive authority 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.

Code example
Shell
cp docs/developers/examples/business-process-contribution/WorkshopProcessWorkActionTest.php \
  packages/workshop/tests/Feature/WorkshopProcessWorkActionTest.php
sh docs/developers/examples/business-process-contribution/verify.sh

The 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

SymptomCauseFixConfirm
Archive note is absent from the BPMN pickerThe descriptor is undiscovered, the App is not installed, or runtime cache is staleCheck the discovery location and installation, then refresh runtime cachesArchive 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 matchRegister workshop with WorkshopProcessActions::ARCHIVE_NOTEverify.sh prints the registration-conformance completion message
The instance stops on an external-task incidentThe instance has no Resource or is anchored to another AppStart it with a canonical workshop.note ResourceRefThe Service Task completes and the instance reaches the End Event
The handler denies the operationThe initiating actor lacks Note archive authority or record scopeCheck the Role and Access Grant in the current Legal EntityThe same actor archives the target Note and its status becomes archived
The Note is archived but a later Gateway sees no valueActivity IO has no output mappingMap status to a Process variable such as archive_statusThe 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
Code example
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
Code example
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
Code example
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
Code example
Shell
#!/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.'