Contribution contracts
Every SDK contribution interface, its required method, and the rules a contributor class must satisfy to be discovered.
Contribution Contracts
Publish App-owned capabilities by implementing an interface in a manifest-discovered class. The example below registers a Resource identity; the inventory tells you which interface publishes each other capability. Discovery succeeds only when the class explicitly implements the interface and its directory is included in App manifest.
Minimal identity contribution
Complete Create a Resource first so
Amuzcorp\Nexia\Workshop\Models\Note exists. This complete class demonstrates
only the Resource identity interface; it requires no base class or trait.
<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\Workshop\Contribution\Resources;
use Amuzcorp\Nexia\Workshop\Models\Note;
use Nexia\Contribution\Contracts\ResourceCatalogContribution;
final class NoteCatalog implements ResourceCatalogContribution
{
public static function resourceKey(): string
{
return 'workshop.note';
}
public static function resourceModelClass(): string
{
return Note::class;
}
}The generated NoteModule already publishes this identity together with its
permissions, authorization, descriptor, and navigation. Keep that generated
module; do not register NoteCatalog alongside it or replace the full module
with this identity-only example. A Resource key must have one owner.
A class is discoverable only through its declared interface and the Manifest's contribution location. A trait supplying methods does not declare an interface.
Contribution interfaces
| Interface | Namespace | Required method |
|---|---|---|
ResourceCatalogContribution | Nexia\Contribution\Contracts | resourceKey(), resourceModelClass() |
ResourceAuthorizationContribution | Nexia\Contribution\Contracts | resourceAuthorization() |
ShellResourceContribution | Nexia\AppRuntime\Contracts | shellResource() |
PermissionContribution | Nexia\Permission\Contracts | catalogPermissionDefinitions() |
RolePresetContribution | Nexia\Permission\Contracts | rolePresets() |
LegalEntityMemberSelfAccessContribution | Nexia\Permission\Contracts | legalEntityMemberSelfAccessKeys() |
NavigationContribution | Nexia\Navigation\Contracts | navigationItems() |
PaletteCommandContribution | Nexia\Palette\Contracts | paletteCommandItems() |
InheritedSearchVisibilityContribution | Nexia\Contribution\Contracts | inheritedSearchVisibilityRelation(), plus Resource Catalog identity |
DashboardWidgetContribution | Nexia\Dashboard\Contracts | dashboardWidgets() |
DashboardQuerySourceContribution | Nexia\Dashboard\Contracts | dashboardQuerySources() |
AgentToolContribution | Nexia\Agent\Contracts | agentTools() |
AppDescriptorContribution | Nexia\AppDescriptors\Contracts | appDescriptors(): AppDescriptorSet |
ResourceSummaryContribution | Nexia\AppDescriptors\Contracts | resourceKey(), summarize() |
BatchResourceSummaryContribution | Nexia\AppDescriptors\Contracts | resourceKey(), summarize(), summarizeMany() |
ResourceReferenceResolutionContribution | Nexia\ResourceReference\Contracts | resourceKey(), resolve() |
ResourceReferenceBatchResolutionContribution | Nexia\ResourceReference\Contracts | Resolution methods plus resolveMany() |
ResourceReferenceSearchContribution | Nexia\ResourceReference\Contracts | search(), plus the resolution methods |
ResourceReferenceEffectiveRangeSearchContribution | Nexia\ResourceReference\Contracts | Search and resolution methods plus searchEffectiveRange() |
ResourceReferencePagedSearchContribution | Nexia\ResourceReference\Contracts | resourceKey(), resolve(), search(), authorized(), searchPage() |
CompositionQueryContribution | Nexia\ResourceComposition\Contracts | compositionQueryProvider(), resourceKey(), resourceModelClass() |
ResourceTransferExportSourceContribution | Nexia\ResourceTransfer\Contracts | resourceTransferExportSources() |
ImportRecipeContribution | Nexia\ResourceImport\Contracts | resourceImportRecipes() |
ResourceImportPipelineContribution | Nexia\ResourceImport\Contracts | resourceImportPipelines() |
SignatureDocumentDataSourceContribution | Nexia\Signature\Contracts | appDescriptors(), signatureDocumentDataSourceProviders() |
SignatureBulkBindingContribution | Nexia\Signature\Contracts | appDescriptors(), signatureBulkBindingProviders() |
CopyableTemplateContribution | Nexia\Templates\Contracts | templates(), apply() |
FixtureContribution | Nexia\Fixture\Contracts | appKey(), fixtureKeys(), seed() |
SetupTaskContribution | Nexia\Setup\Contracts | tasks() |
FeatureGuideContribution | Nexia\Guidance\Contracts | ownerKey(), guides() |
SelfWorkContextContribution | Nexia\SelfService\Contracts | workContexts() |
SelfServiceActionItemContribution | Nexia\SelfService\Contracts | selfServiceActions() |
A contribution carries exact catalog keys — labelKey,
titleKey, descriptionKey — and the host resolves them for the requested locale
at the payload boundary. Backend contributions supply keys rather than loading a locale map.
Most methods are static. AppDescriptorContribution::appDescriptors() is the
one static publication boundary for every typed descriptor; the immutable set
may contain Resource, Slot Widget, Approval, Signature, Process, Decision, or
Reporting descriptors together. Descriptor keys must be non-empty and have no
surrounding whitespace. Core preserves FQCN contributor order and then each set's
declaration order; host-only test registrations follow in call order.
ResourceSummaryContribution, the
ResourceReference interfaces, CopyableTemplateContribution, and
FixtureContribution use instance methods because they resolve, apply, or seed
work per request or per run. SetupTaskContribution is also instance-based:
the host resolves the contributor through its container, infers its owner from
the manifest discovery record, and registers the returned declarations.
The two Signature contributions publish static descriptors through
appDescriptors() and instance runtime providers through their second method;
Core joins each exact descriptor/provider identity and fails closed when the
provider is absent.
ResourceSummaryContribution is also the record-presentation boundary used by
compact host surfaces such as Spotlight. The owner returns an authorized
display, optional route, optional Shell icon name, and typed fields. A field may set
ResourceSummaryFieldRole::Subtitle, Meta, or Status so a compact consumer
knows the field's semantic placement; the host still owns the React component,
spacing, truncation, and accessibility. Use BatchResourceSummaryContribution
when the provider can summarize several resource IDs in one bounded query. Core
keeps the single-record fallback for existing providers. Spotlight passes
ResourceVisibility::Search only after it re-fetches and authorizes the search
candidate, and the owner must still enforce record visibility or return null.
App-owned Agent tools
Use AgentToolContribution only when generic Resource data, a Dashboard query
source, and an existing screen action cannot express the capability. The App
owns a normal authorized Laravel route and publishes only immutable metadata:
use Nexia\Agent\Contracts\AgentToolContribution;
use Nexia\Agent\AgentToolDeclaration;
use Nexia\Agent\AgentToolLane;
use Nexia\Agent\AgentToolRouting;
use Nexia\Agent\AgentToolSurface;
use Nexia\Agent\AgentToolTier;
final class TimeAbsenceAgentTools implements AgentToolContribution
{
public static function agentTools(): array
{
return [new AgentToolDeclaration(
key: 'time-absence.personal_time_leave.read',
tier: AgentToolTier::Auto,
routing: AgentToolRouting::read(
AgentToolLane::DirectData,
AgentToolSurface::Data,
),
description: 'Read the current user personal leave balance.',
method: 'GET',
path: '/api/legal-entities/{legalEntity:public_id}/time-absence/personal-leave',
permissions: ['time-absence.leave_balance.read'],
)];
}
}The first key segment is the stable App namespace and may be kebab-case. Every
remaining dotted segment is lowercase alphanumeric/underscore. permissions
is non-empty and must describe the real route guard. inputSchema, when
present, is a non-empty JSON Schema object. Laravel scoped placeholders such as
{legalEntity:public_id} keep their exact route syntax; the Gateway substitutes
the active Legal Entity or an argument whose name exactly matches the placeholder
before invocation. A generic id argument never aliases {user}, {ticket}, or
another differently named placeholder.
Preconditions are exact Router capabilities rather than free-form tags.
legal_entity_selected is satisfied only when the trusted delegation context
contains an active Legal Entity; without it the Router withholds the tool. An
unsupported precondition also keeps the tool unavailable.
Discovery calls the static method without resolving the contributor through
the Core container. The declaration can publish only a Laravel route binding,
routing lane/effect/surface, default confirmation tier, permission metadata,
and invalidation hints. It cannot publish a Gateway handler, Closure, Core
Action, recipe, host widget, or browser tool. External effects additionally
require AgentToolTier::Confirm and the external-share facet.
Use this extension only for distinct business capabilities. Ordinary Resource CRUD and declared domain actions use Resource descriptors and the fixed generic data tools; they do not need AgentToolContribution.
Interface inheritance
Four interfaces extend ResourceCatalogContribution, so implementing them also
requires resourceKey() and resourceModelClass():
| Interface | Also requires |
|---|---|
ResourceAuthorizationContribution | resourceKey(), resourceModelClass() |
ShellResourceContribution | resourceKey(), resourceModelClass() |
DashboardWidgetContribution | resourceKey(), resourceModelClass() |
InheritedSearchVisibilityContribution | resourceKey(), resourceModelClass() |
A dashboard widget contributor is therefore bound to a Resource by design.
Traits that supply methods
| Trait | Namespace | Implements | Reads from |
|---|---|---|---|
ContributesResourcePermissions | Nexia\Permission\Concerns | catalogPermissionDefinitions() | permissionResources() |
ContributesNavigationDestination | Nexia\Navigation\Concerns | navigationItems() | $navigation |
ContributesNavigationDestinationActions | Nexia\Navigation\Concerns | agent action facet | agentNavigationActionDefinitions() |
HasShellResource | Nexia\AppRuntime\Concerns | shellResource() | Catalog identity |
You still declare the interface. Using ContributesResourcePermissions without
implements PermissionContribution produces a class with a working method that
discovery never selects — the doctor reports permission contributors: 0 classes.
Requirements a contributor must satisfy
| Requirement | Why |
|---|---|
Inside a declared contributionLocations() root | Manifest generation indexes only those directories |
Not abstract | Abstract classes are skipped |
| File name matches class name | PSR-4 autoloading |
| Keys App-prefixed and stable | Keys appear in permission rows, translations, and stored tenant state |
| Labels from the App locale catalog | A literal string ships untranslated |
| No tenant writes during discovery | The registry is assembled before any actor or tenant context exists |
| Safe to call repeatedly | Consumer registries may read declarations more than once |
The last two matter most. Declaration methods, including
SetupTaskContribution::tasks(), run while a registry is being assembled — no
tenant is initialized and no actor is known. Reading the database, resolving
the current user, or writing anything there will fail or corrupt the registry.
Setup evaluators are a separate phase: the host calls evaluate() only while
building a tenant plan and supplies an opaque tenant, actor, locale, and time
context. The contributor still performs no tenant work during discovery.
Output or return: Descriptor value objects
AppDescriptorContribution returns an immutable AppDescriptorSet of typed
objects, all in Nexia\AppDescriptors. The host's AppDescriptorCatalog
discovers that single contract, retains contributor provenance, and lets each
consumer request only its descriptor class:
| Class | Required constructor arguments |
|---|---|
ResourceDescriptor | key, version; optional labelKey naming the resource itself |
EventDescriptor | full Event key, positive integer schemaVersion, aggregateType, payloadSchema |
SlotWidgetDescriptor | key, version, slot, component, slotApiVersion |
ApprovalFormBindingDescriptor | appKey, resourceKey, actionKey, labelKey, entryModes, formWidgetKey, documentSchemaKey |
ApprovalDocumentSchema | appKey, resourceKey, sections |
ApprovalBusinessTemplatePresetDescriptor | App-owned preset key and business-template fields |
ApprovalRoutePolicyPresetDescriptor | App-owned preset key and route-policy fields |
SignatureTemplateBindingDescriptor | App-owned signature-template binding fields |
ProcessStartBindingDescriptor | appKey, bindingKey, definitionKey, processId, resourceKey, startPermissionKey, labelKey |
ProcessWorkActionDescriptor | kind, appKey, actionKey, labelKey, topic; optional payload/input, approvalTask, and outputContract metadata |
ProcessApprovalTaskConfiguration | bindingKey, outcomeTopic; optional outcomes, payload keys, businessTemplatePresetKey, and routePolicyPresetKey |
ProcessUserTaskFormDescriptor | key, appKey, labelKey |
ProcessTemplateDescriptor | key, version, appKey (nullable, no default), labelKey, category, structure |
DecisionResultTemplateDescriptor | key, version, labelKey, fields; optional descriptionKey, decisionTable, hitPolicy |
ReportingViewDescriptor | key, version, viewName, columns |
Versioned top-level descriptors carry status: DescriptorStatus, normally
defaulting to Active, with Deprecated and Removed as the other states.
Most use a string version; EventDescriptor instead requires the positive
integer schemaVersion carried by EventEnvelope.
These descriptor types remain distinct typed values. The generic set replaces their separate discovery interfaces and registry entry points; it does not remove Approval presets, Signature bindings, Public Event contracts, or any Process/DMN capability.
Descriptor constructors validate their own values eagerly. The strict host gate
validates the contents of each set, enforces declared descriptor types,
App-owned key prefixes, and per-family key uniqueness. A standalone public
integration Event uses EventDescriptor; a public Resource lifecycle Event
stays nested under ResourceDescriptor and must not be declared a second time.
EventPayloadSchema accepts only the supported scalar, array, and object types,
treats fields as required unless required: false, and rejects undeclared
payload fields. Composable payload schemas additionally require labels through
PublicEventPayloadSchema; use PublicEventPayloadSchema::withLabelKeys() when
one translation key can be derived for each path.
An optional App with a malformed contribution is isolated while the registry is
assembled so unrelated Apps can still boot. That tolerance is not an acceptance
gate: nexia-apps:validate-app-descriptors, App activation, and tenant installation
all run strict validation and refuse the invalid App before synchronization or
tenant state writes.
Errors
| Symptom | Cause | Resolution |
|---|---|---|
WARN contribution locations: no package contribution location is registered | Directory not declared, or namespace mismatch | Fix contributionLocations() |
WARN permission contributors: 0 classes | Trait used without declaring the interface | Add implements PermissionContribution |
WARN permissions discovered: 0 permission definitions | No discovered contributor declares permissions | Check the interface, then the class location |
InvalidArgumentException from a descriptor | Descriptor constructor validation | Read the message; it names the invalid value |
DescriptorValidationException during validation, activation, or installation | A contribution is malformed or contradicts another contribution | Fix every reported descriptor path, then rerun nexia-apps:validate-app-descriptors |
| Contribution appears in some processes only | A stale autoloader in a long-running process | Restart workers and Vite after adding a class |
Use in your App
Keep the contributor inside a directory returned by your manifest and explicitly implement its interface. A trait alone does not register a contribution.
Related
- Extension Model — discovery, the four gates, and the host-only boundary
- Resource contracts — the Resource catalog contract in depth
- Business Process contracts — Process descriptor details
- Electronic Approval contracts — Approval binding and schema details
- Contribute Setup tasks — declaration, evaluator, dependency, and operational-state rules
- Fix contribution discovery — each error above as a diagnosis