Copyable templates
Offer optional starter content an actor explicitly copies into tenant ownership, and understand why the copy never tracks your source.
Offer optional starter records that an operator explicitly copies into tenant ownership.
Declare and apply the template
Implement SDK CopyableTemplateContribution: templates() returns the versioned source, and apply() writes App-owned records and returns TemplateApplicationResult with public references. Validate the template key and source shape, respect its tenant or Legal Entity scope, and preserve existing customized rows when retrying. The existing Talent implementation is packages/talent/src/Contribution/TalentDataTemplates.php.
Apply through the host with an explicit tenant and actor; Installation content documents the command and scope contract. Required operational rows instead follow Required installation content.
Set TEMPLATE_TENANT_ID and TEMPLATE_ACTOR_ID to the target tenant ID and its authorized user ID, then apply from the CLI:
task artisan -- nexia-apps:apply-template payroll smb_basic_elements \
--tenant="$TEMPLATE_TENANT_ID" --actor="$TEMPLATE_ACTOR_ID"Confirm the result
The ledger records the source/version and resulting references, the tenant may edit the copied records, and reapplying does not duplicate them. Shipping a new template version must not silently update existing copies. Exercise those cases with Test an App.
Application and provenance details
A copyable template is content a tenant may choose to copy. Applying it creates editable, tenant-owned rows and records provenance.
packages/payroll/src/Contribution/PayrollDataTemplates.php:
<?php
declare(strict_types=1);
namespace Amuzcorp\Nexia\Payroll\Contribution;
use Amuzcorp\Nexia\Payroll\Domain\PayrollElementCatalog;
use Amuzcorp\Nexia\Payroll\Models\PayrollElement;
use Nexia\Templates\Contracts\CopyableTemplateContribution;
use Nexia\Templates\CopyableTemplate;
use Nexia\Templates\TemplateApplicationContext;
use Nexia\Templates\TemplateApplicationResult;
/** Tenant-copyable SMB baseline; applying it creates editable Payroll-owned records. */
final class PayrollDataTemplates implements CopyableTemplateContribution
{
private const KEY = 'smb_basic_elements';
public function templates(): array
{
return [new CopyableTemplate('payroll', self::KEY, '2', CopyableTemplate::SCOPE_TENANT, PayrollElementCatalog::definitions())];
}
public function apply(CopyableTemplate $template, TemplateApplicationContext $context): TemplateApplicationResult
{
if ($template->key !== self::KEY || ! is_array($template->source)) {
throw new \InvalidArgumentException('Unsupported Payroll data template.');
}
$refs = [];
foreach ($template->source as $definition) {
$element = PayrollElement::query()->firstOrCreate(
['code' => $definition['code']],
$definition,
);
$refs[] = ['type' => 'payroll.payroll_element', 'id' => (string) $element->public_id, 'key' => $element->code, 'version' => 1];
}
return new TemplateApplicationResult($refs);
}
}Two instance methods: templates() declares what may be copied, apply() performs
the copy and returns references to the rows it created.
Contribution discovery
The contribution point is public at
Nexia\Templates\Contracts\CopyableTemplateContribution. A new App may
implement it directly; the host discovers the class through the App's declared
contribution locations.
The SDK also publishes CopyableTemplate, TemplateApplicationContext, and
TemplateApplicationResult, so the complete declaration and application
boundary stays under Nexia\*. packages/payroll and packages/talent are the
current implementations to copy.
The value object
CopyableTemplate requires a non-empty app, key, and version, plus a scope
from two constants:
| Constant | Value |
|---|---|
CopyableTemplate::SCOPE_TENANT | tenant |
CopyableTemplate::SCOPE_LEGAL_ENTITY | legal_entity |
It throws on anything else:
A copyable template requires an app, key, and version.
Unsupported copyable template scope [organization].
Three properties of a correct apply()
| Property | How | Why |
|---|---|---|
| Idempotent | firstOrCreate keyed on a business identifier | Applying twice must not duplicate |
| Returns references | ['type', 'id', 'key', 'version'] with public_id | Provenance, never the numeric key |
| Validates its input | Reject a key it does not own | Do not assume the runtime filtered correctly |
The provenance ledger
Applying a template records an entry in the tenant-local template_applications
ledger:
| Field | Content |
|---|---|
| App key | Owning App |
| Template key and version | The immutable source identity |
| Scope | tenant or legal_entity |
| Legal Entity | Nullable |
| Actor | Who applied it |
| Result status | Success or failure |
| Failure reason | On failure |
| Created resource references | What the copy produced |
The ledger is provenance only. It never becomes the template source catalog, and it does not track later edits to the copies.
Updating sources and copies
A copy diverges the moment it is created. Never ship a fix by editing a template and expecting existing tenants to receive it.
| You do | Effect on existing copies |
|---|---|
| Edit the template source | Nothing |
Bump version | Nothing — only future applications differ |
| Retire the template | Nothing — the copies are the tenant's |
To get new content to an existing tenant, someone applies the new version. Because
apply() is idempotent on a business key, that adds only what is missing.
Two current constraints
Approval form presets are tenant-scoped. Passing --legal-entity to
nexia-apps:apply-template fails for them.
Process templates do not use this path at all. Selecting one changes the unsaved BPMN editor document; normal definition save and publish owns persistence and paired-decision deployment. See Business Process.
Boundaries
Not an initializer. A template is skippable by design. If the App is broken without the rows, they belong in installation.
Not demo data. Demo data is disposable, never an installation prerequisite, and
rejected in production without --force.
Nothing here grants authority. Copied rows are subject to normal authorization.
The source is immutable in effect. Treat a published key plus version as
fixed; change behavior with a new version.