Skip to content
Guide

Contribute Setup tasks

Add App-owned readiness tasks to the tenant Setup plan without coupling the App to Core internals.

Add one readiness task to the tenant's existing Setup plan. Your App supplies a condition evaluator and a link to the configuration that satisfies it.

Short implementation path

  1. Implement SetupTaskContribution and return a SetupTaskDefinition with a stable App-prefixed key, revision, evaluator class, and translated title/description.
  2. Have SetupTaskEvaluator inspect App-owned evidence without changing it. Choose Live when readiness can regress, or Latched with acknowledgement when completion should persist.
  3. Link an existing App route using SetupTaskAction, tenant-scoped permissions, and a narrow recheckAfter matcher for the change that satisfies the task. Setup has no Legal Entity or Operating Unit authorization context.
  4. Refresh contribution discovery and open Setup in an operational tenant. Save the required configuration through its protected API and reload the plan.

Confirm the result

The task is visible when its App is operational, its action is available only with the declared permissions, and its evaluated state changes with the evidence. Completing a guide alone does not complete a task. Follow Test an App for the focused check.

The People example below includes declaration, evidence evaluation, revision handling, and mutation observation. If the configuration screen does not exist yet, start with Add a settings page.

1. Declare the task

Implement Nexia\Setup\Contracts\SetupTaskContribution below a manifest contribution location. This complete one-task example uses People Core's worker-numbering evaluator and omits optional section grouping:

Code example
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\PeopleCore\Contribution;

use Amuzcorp\Nexia\PeopleCore\Setup\WorkerNumberingSetupTaskEvaluator;
use Nexia\Mutation\MutationMatcher;
use Nexia\Mutation\MutationOperation;
use Nexia\Setup\Contracts\SetupTaskContribution;
use Nexia\Setup\Data\SetupTaskAction;
use Nexia\Setup\Data\SetupTaskDefinition;
use Nexia\Setup\Enums\SetupTaskCompletionMode;
use Nexia\Setup\Enums\SetupTaskImportance;

final class PeopleSetupTasks implements SetupTaskContribution
{
    public function tasks(): array
    {
        return [
            new SetupTaskDefinition(
                key: 'people.worker_numbering',
                revision: 1,
                priority: 100,
                titleKey: 'people.setup.worker_numbering.title',
                descriptionKey: 'people.setup.worker_numbering.description',
                importance: SetupTaskImportance::Required,
                completionMode: SetupTaskCompletionMode::Live,
                skippable: false,
                requiresAcknowledgement: false,
                dependencies: [],
                evaluatorClass: WorkerNumberingSetupTaskEvaluator::class,
                defaultAction: new SetupTaskAction(
                    '/apps/people/worker-number-rules',
                    ['people.worker_number_rule.read'],
                    recheckAfter: new MutationMatcher(
                        resourceKeys: ['people.worker_number_rule'],
                        operations: [
                            MutationOperation::Created,
                            MutationOperation::Updated,
                            MutationOperation::Deleted,
                        ],
                    ),
                ),
            ),
        ];
    }
}

Import MutationMatcher and MutationOperation from Nexia\Mutation. The matcher is independent of the route: it names the domain change that should wake the guided agent after the user saves.

The complete declaration is in packages/people/src/Contribution/PeopleSetupTasks.php.

The host derives App Family, App name, and ordering from the manifest's AppDefinition. The contributor declares none of those values. It may define $basics as a SetupTaskSection when its own task list benefits from another grouping layer; otherwise omit section. Repeated section metadata within one App must match. Section priority, task priority, and finally task key control deterministic ordering inside that App.

Use Required or Recommended for product guidance. Set skippable only when an Account Owner may deliberately treat the task as terminal without satisfying its evidence. On a skippable task, optionally set skipGuidanceKey to a localized explanation of when skipping is safe. The host carries that text into the snapshot; it does not authorize the skip or decide completion. Core tasks may depend only on Core tasks. An App task may depend on a Core task or another task owned by the same App, never on a different App.

2. Evaluate App-owned evidence

The contributor is declaration-only and must not query the tenant database. Put domain queries in a SetupTaskEvaluator owned by the App. The People worker number evaluator uses the immutable evaluation time and App-owned models:

Code example
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\PeopleCore\Setup;

use Amuzcorp\Nexia\PeopleCore\Enums\WorkerNumberPurpose;
use Amuzcorp\Nexia\PeopleCore\Models\WorkerNumberRule;
use Nexia\Setup\Contracts\SetupTaskEvaluator;
use Nexia\Setup\Data\SetupCriterion;
use Nexia\Setup\Data\SetupEvaluationContext;
use Nexia\Setup\Data\SetupTaskAssessment;

final class WorkerNumberingSetupTaskEvaluator implements SetupTaskEvaluator
{
    public function evaluate(SetupEvaluationContext $context): SetupTaskAssessment
    {
        $configured = WorkerNumberRule::query()
            ->where('purpose', WorkerNumberPurpose::Worker->value)
            ->whereNull('legal_entity_id')
            ->effectiveOn($context->evaluatedAt->format('Y-m-d'))
            ->exists();

        return new SetupTaskAssessment(
            applicable: true,
            criteria: [new SetupCriterion(
                'active_effective_tenant_worker_rule',
                'people.setup.worker_numbering.criteria.active_effective_tenant_worker_rule',
                $configured,
            )],
        );
    }
}

The implementation is in packages/people/src/Setup/WorkerNumberingSetupTaskEvaluator.php. SetupEvaluationContext contains opaque tenant and actor keys, locale, one immutable evaluation time, the current task key, and an optional host-provided subject key; it does not expose a Core model. App declarations normally leave the subject key unset. Use evaluatedAt for effective-date rules so every criterion in one snapshot uses the same instant.

Return ordinary missing configuration as an unsatisfied required criterion, not as a blocker. A blocker means the task cannot proceed, such as an unavailable external prerequisite. Return applicable: false only when the task genuinely does not apply to that tenant; the host omits it from the payload.

3. Choose completion and revision behavior

The host derives pending, in_progress, blocked, completed, and skipped. An App returns evidence, not a status.

ModeUse it whenStored behavior
LiveCompletion must follow current domain evidenceRe-evaluates on every snapshot and may regress when evidence changes
LatchedA deliberate acceptance must remain completeRequires acknowledgement and retains the completion latch for that task revision

Task identity and revision determine what happens during updates:

ChangeTenant-visible result
Add a taskIt joins the same get-started plan on the next read when the App is operational
Change evaluator logic with the same key and revisionLive evidence changes immediately; matching stored interaction facts still apply
Raise the task revisionOld interaction facts are ignored; the next accepted interaction resets the row to the new revision
Make a task inapplicable or make the App non-operationalThe task is omitted, but omission alone does not delete its state row
Make the App operational again with the same key and revisionThe task returns to the same plan and retained matching interaction state applies again
Rename or remove a task keyThe old row remains unused; a renamed key is a new task identity

Raise revision only when starts, skips, acknowledgements, or completion latches recorded under the previous contract must no longer apply. Do not raise it for copy, ordering, or evaluator changes whose existing interactions remain valid.

The plan's own revision is response metadata. It does not invalidate task state and is not an append-only history. The current get-started plan remains at revision 1 while this contract is still under development. Installing or reinstalling an App does not create another plan record; operational App tasks are composed into the one host plan on the next evaluation.

4. Observe plan updates and App contributions

GET /api/setup/plans/get-started is write-free. It can be prefetched and retried without assigning tasks or clearing a NEW indication. The Shell calls POST /api/setup/plans/get-started/open only after Setup has opened. That transaction records setup_plan_states (seen revision and open time) and the currently applicable task assignments in setup_task_states (first/last seen, source owner/kind, and assignment plan revision), while retaining the existing interaction facts.

The open response is deliberately the snapshot from before that write, so the first rendered open still shows its update notice and NEW task/source. A later read or reopen no longer marks those assigned tasks new. A tenant that has never opened Setup is a baseline: every currently applicable task is is_new: false, including old live-completed tasks without an interaction row.

For a tenant with an existing baseline, an applicable task without an assignment is new. Core tasks identify new_source.kind as core_contribution; App tasks identify it as app_contribution with their opaque owner key. These values identify the contributor, not the exact installation, activation, release, or plan-revision event that made the task visible. A same-key task revision mismatch is is_updated; its first open returns that pre-write state, then clears only that task's stale interaction facts and records the new revision. The host does not need to know an App namespace or domain model to make either decision.

Core may also contribute a Platform-placed task that acts on a specific App. Such a payload keeps app: null and exposes subject_app with the canonical App identity and ordering metadata. A host task that summarizes several Apps instead exposes subject_apps in canonical Family and launcher order. The current core.initial_app_access task uses that aggregate form and emits one required initial_access_decision.<app-key> criterion for every active, initialized, code-loaded App. The Shell presents one reviewed/total task and keeps the per-App status in its expanded detail. This is host-only composition metadata; an App contribution does not declare either subject field.

The aggregate action is App-aware as well. For the first unsatisfied criterion in canonical Family and launcher order, the Shell opens /settings/apps?step=access&app={app-key}. The App Catalog reads that App's non-technical, non-deprecated Role Presets for tenant and current-Legal-Entity scope. In each authorized scope the administrator selects presets and users; every selected user receives every selected Role. When both scopes are selected, the tenant bootstrap runs with finalize: false and only the final successful Legal Entity bootstrap records configured, so a later failure leaves the criterion incomplete and retryable. A protected preset that the actor cannot self-approve is prepared without a Grant and reported for separate approval. POST /api/apps/{appKey}/initial-access-decision accepts only not_needed; configured is evidence written by a successful Role bootstrap. After either result, the catalog advances to the next pending App.

The host also contributes core.data_migration as one tenant-level onboarding decision, not one Setup task per App, migration target, or stage. It is a revision-2 Recommended, skippable Live task. Its data section metadata orders it after organization setup and before identity and access, while its only dependency is core.legal_entity_profile. An actor with the tenant-scoped system.data.import permission can choose Review available imports to open /settings/data-migrations, or explicitly skip with Start without migration. Each execution is persisted in the tenant database with stable stage, provider, and target keys. A clean completed run automatically completes Setup; processing, failed, and completed-with-errors attempts remain in progress. App backends record the same evidence through the app-neutral SDK recorder, and frontend onCompleted is only a refetch hint.

The Shell renders Platform tasks as one flat Basic settings list even though the payload retains Core section metadata for deterministic ordering and other consumers. App-owned tasks keep the Family, App, and optional App-section hierarchy declared through their normal placement contracts.

Do not use this mechanism for Operational Health, per-record compliance, cross-tenant projection, or Account Owner delegation. Beta Setup remains an Account Owner-only readiness surface.

5. Add actions, permissions, recheck conditions, and translations

SetupTaskAction::routeTarget is opaque App-owned navigation data. Point it at the App's public Shell route, not a package path or Core-internal alias. The host evaluates requiredPermissionKeys as tenant-scoped permissions. A Legal-Entity- or Operating-Unit-scoped permission cannot make this Setup action executable.

Action permission controls only whether the checklist offers navigation. It does not complete or block the task, authorize the Setup endpoints, or replace authorization at the destination API. The destination still enforces its own read and write policy.

SetupTaskAction::recheckAfter is optional. Add it when Agent-guided setup should continue after the user's real save. A matcher names exactly one subject family:

Change ownerMatcher
A Resource Catalog modelresourceKeys plus one or more of Created, Updated, Deleted, Restored; optional resourceIds narrow the match
A successful semantic action with no Resource rootactionKeys plus only Succeeded

Keys and operations inside a matcher use OR semantics. Use the task's own stable Resource Key whenever possible. The host automatically observes committed Eloquent lifecycle events for every catalog-bound model, so an ordinary generated save/delete path needs no controller hook. A query-builder bulk update or another path that bypasses model events injects Nexia\Mutation\Contracts\MutationPublisher and calls resourceChanged() after the business write succeeds. A non-resource action calls actionSucceeded() after success.

The host rejects unknown Resource Keys, an App task that names another App's Resource, and action keys outside the task owner's prefix. Do not match the route, active tab, HTTP method, or a generic "save" event. A mutation is only a wake-up hint: after core.mutation.await returns, the agent reads the plan and the evaluator decides completion again. A save on another route can wake this task only when it committed the same declared Resource or semantic action.

For a task with requiresAcknowledgement: true, the Agent-guided flow records the interaction only after an observed wait and a fresh plan read confirms the same next task, no blockers, and every required criterion satisfied. It then calls the AUTO-tier core.setup.task.acknowledge, which requires tenant.setup.update, changes only Setup interaction state, and is followed by another plan read. A recheck, timeout, reconnect, or matcher alone never authorizes acknowledgement.

Add every App-owned section, task, criterion, and blocker key to all App locale catalogs. The Shell loads catalogs for installed Apps when it renders the plan. App Family and App labels already come from the canonical App definition and must not be redeclared for Setup.

6. Cover the package contract

Test the contributor and evaluator in the App package. At minimum, assert:

  • the manifest discovers the contributor and the host infers the owning App;
  • task keys, revisions, ordering, action routes, permissions, and mutation matchers are exact;
  • every required criterion is false and true at its real boundary;
  • effective end dates use the domain's inclusive or exclusive rule correctly;
  • all supported locale catalogs contain the declared keys;
  • inactive or otherwise invalid domain records do not accidentally complete the task.

Host tests already cover owner-prefixed keys, duplicate keys, dependency policy, cycles, operational App filtering, status precedence, interaction persistence, revision invalidation, and tenant isolation. Add a host test only when changing one of those host-owned contracts.

Verify

Run the production example's focused package test:

Code example
Shell
task test -- packages/people/tests/Feature/PeopleSetupTasksTest.php --sequential --compact

It should pass the contribution shape, all four evaluator boundaries, and locale parity checks. When implementing a different App, run the equivalent focused test in that package.

Then verify the tenant lifecycle manually:

CheckExpected result
Read the plan before the App is operationalThe App task is absent
Install and initialize the App after a Setup baseline, then reopen SetupThe task appears inside get-started as NEW with the App owner/source; no second plan is created
Satisfy a live criterion and reopen SetupThe task becomes completed
Start Agent-guided setup, then save an unrelated ResourceThe parked agent stays waiting
Save the task's matched Resource from another routeThe agent wakes, reads the plan again, and trusts only the evaluator result
Remove the evidence and reopen SetupA live task regresses to an incomplete state
Make the App non-operational, then operational againThe task disappears and returns; matching retained interactions still apply and it is not NEW again
Test an actor without the action permissionThe task remains visible, but its navigation action is unavailable

Common mistakes

Persisting the evaluator result. The host evaluates current App evidence on every snapshot. Persist only the App's domain truth; Core stores Setup interaction and assignment-observation facts separately.

Bumping the plan revision to reset one task. Plan revision does not reset task state. Raise that task's revision.

Bumping a task revision for every code change. That discards valid skip, acknowledgement, and latch history on the next interaction. Raise it only for an interaction-contract change.

Using a Legal Entity permission for the action. Setup action decisions have tenant scope, so the action remains unavailable even if the actor has a scoped grant elsewhere.

Depending on another App's task. The registry rejects cross-App dependencies. Depend on Core or on the same owner only.

Using the open route or every successful request as the save signal. Routes are navigation, not domain identity. Declare the narrow Resource or semantic action matcher and let the host ignore unrelated saves.

Treating a matched mutation as completion. It only wakes the agent. Always re-read the authoritative plan because the evaluator may still report missing criteria.

Acknowledging after recheck. Recovery means the browser may have lost a signal; it is not user-save evidence. Acknowledge only after observed and the fresh evaluator checks described above.

Treating missing configuration as a blocker. Missing ordinary evidence is an incomplete criterion. Reserve blockers for conditions that prevent progress.

Source reference

packages/people/src/Contribution/PeopleSetupTasks.php contains additional tasks and optional section and skip guidance. packages/people/tests/Feature/PeopleSetupTasksTest.php covers their metadata, evidence boundaries, and locale keys.

Next

Source of truth: docs/developers/content/en/platform-extensions/setup-plans.md