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
- Implement
SetupTaskContributionand return aSetupTaskDefinitionwith a stable App-prefixed key, revision, evaluator class, and translated title/description. - Have
SetupTaskEvaluatorinspect App-owned evidence without changing it. Choose Live when readiness can regress, orLatchedwith acknowledgement when completion should persist. - Link an existing App route using
SetupTaskAction, tenant-scoped permissions, and a narrowrecheckAftermatcher for the change that satisfies the task. Setup has no Legal Entity or Operating Unit authorization context. - 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:
<?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:
<?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.
| Mode | Use it when | Stored behavior |
|---|---|---|
Live | Completion must follow current domain evidence | Re-evaluates on every snapshot and may regress when evidence changes |
Latched | A deliberate acceptance must remain complete | Requires acknowledgement and retains the completion latch for that task revision |
Task identity and revision determine what happens during updates:
| Change | Tenant-visible result |
|---|---|
| Add a task | It joins the same get-started plan on the next read when the App is operational |
| Change evaluator logic with the same key and revision | Live evidence changes immediately; matching stored interaction facts still apply |
| Raise the task revision | Old interaction facts are ignored; the next accepted interaction resets the row to the new revision |
| Make a task inapplicable or make the App non-operational | The task is omitted, but omission alone does not delete its state row |
| Make the App operational again with the same key and revision | The task returns to the same plan and retained matching interaction state applies again |
| Rename or remove a task key | The 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 owner | Matcher |
|---|---|
| A Resource Catalog model | resourceKeys plus one or more of Created, Updated, Deleted, Restored; optional resourceIds narrow the match |
| A successful semantic action with no Resource root | actionKeys 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:
task test -- packages/people/tests/Feature/PeopleSetupTasksTest.php --sequential --compactIt 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:
| Check | Expected result |
|---|---|
| Read the plan before the App is operational | The App task is absent |
| Install and initialize the App after a Setup baseline, then reopen Setup | The task appears inside get-started as NEW with the App owner/source; no second plan is created |
| Satisfy a live criterion and reopen Setup | The task becomes completed |
| Start Agent-guided setup, then save an unrelated Resource | The parked agent stays waiting |
| Save the task's matched Resource from another route | The agent wakes, reads the plan again, and trusts only the evaluator result |
| Remove the evidence and reopen Setup | A live task regresses to an incomplete state |
| Make the App non-operational, then operational again | The task disappears and returns; matching retained interactions still apply and it is not NEW again |
| Test an actor without the action permission | The 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
- Contribution contracts — contributor discovery and ownership rules
- SDK contracts — public namespace and package boundaries
- App manifest — contribution location declaration
- Add a protected API — enforce the action destination