Skip to content
Guide

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:

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

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

ConstantValue
CopyableTemplate::SCOPE_TENANTtenant
CopyableTemplate::SCOPE_LEGAL_ENTITYlegal_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()

PropertyHowWhy
IdempotentfirstOrCreate keyed on a business identifierApplying twice must not duplicate
Returns references['type', 'id', 'key', 'version'] with public_idProvenance, never the numeric key
Validates its inputReject a key it does not ownDo not assume the runtime filtered correctly

The provenance ledger

Applying a template records an entry in the tenant-local template_applications ledger:

FieldContent
App keyOwning App
Template key and versionThe immutable source identity
Scopetenant or legal_entity
Legal EntityNullable
ActorWho applied it
Result statusSuccess or failure
Failure reasonOn failure
Created resource referencesWhat 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 doEffect on existing copies
Edit the template sourceNothing
Bump versionNothing — only future applications differ
Retire the templateNothing — 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.

Source of truth: docs/developers/content/en/platform-extensions/copyable-templates.md