Actor Slot Values
Core lets an artifact say "I need to know the currency" — a slot, declared on a module, resolved per request (Slots). The SDK adds the layer Core cannot have: a value a person carries. Alice works in EUR and thinks in quarters; Bob in CHF and months. Neither sets it per screen, and no report has to ask.
Slot or setting?
Both are configuration, and mixing them up costs a refactor. The difference is what the value belongs to:
#[Setting] | #[Slot] + an actor value | |
|---|---|---|
| Answers | how is this artifact configured | in what context am I working |
| Declared on | any configurable class — a handler, a provider, an artifact | the module class, and nowhere else |
| Types | 13, including arrays and Model | 7 scalars — no arrays, no Model |
| Precedence | the scope ladder: Platform < Installation < Tenant < Site < User | one value per actor, or the catalog default |
| Stored in | sdk_settings | sdk_slot_assignments |
| Read as | $context->setting('key') | a resolved map, handed to you (below) |
A rule of thumb from the specs: settings are for preferences, actor slot values are for business context.
Where the actor's value sits
The SDK inserts one source into Core's chain — second, right behind the request:
Request → Actor → Composition → Setting (the catalog default)First answer wins, and a slot that has been filled is never offered to a later source. So a URL parameter still overrides the person's own value — that is the point of the order: the request is the most specific statement of intent, the actor's value is the standing one, and the catalog default is the floor. Because the default always answers, a catalogued slot always resolves.
Two properties of the actor layer worth knowing:
- One value per actor and slot. There is no history, no validity window and no supersession: a write replaces, a clear deletes.
- It is inert without an actor. An anonymous request skips the source entirely rather than failing.
Setting a value
In the Partners app
The SDK ships the surface: Partners → a partner → Parameters. The tab lists every catalogued slot with its type, its note, its default and the partner's own value, and lets an administrator create, edit or clear one. It needs the setSlotOverride / clearSlotOverride abilities, which come with local_admin.
That is the everyday path, and it is deliberately an administrator's path: the SDK has no self-service screen for a person's own context yet (the Launchpad's profile is where it will live).
From your own code
use LaravelUi5\Sdk\Parameters\Contracts\ActorParameterWriterInterface;
final class SwitchCurrencyHandler implements SdkActionHandlerInterface
{
public function __construct(private ActorParameterWriterInterface $writer) {}
public function handle(
BusinessTransaction $transaction,
BusinessContextInterface $context,
SdkContext $sdk,
): ActionResponse {
$this->writer->set($sdk->actor(), 'currency', 'CHF', $sdk);
return $transaction->commit();
}
}set($forActor, $name, $value, $context) and clear($forActor, $name, $context) are the whole write API. $forActor need not be the acting partner — writing on behalf of someone is first-class, and the row records who did it.
The writer authorizes nothing. It validates, it does not gate — so the gate is your action's #[Act] ability, exactly as it is for the two shipped actions.
EditLevel does not govern this
#[Slot(editable: …)] looks like a write permission and is not one. Nothing checks it when an actor value is written; it survives as the grade of the synthetic setting Core generates per slot, and the Partners table renders it as a badge. If a slot must be harder to change than another, put that in the ability of the action that changes it.
What a write refuses:
| Situation | Result |
|---|---|
| the slot is not in the live catalog | UnknownSlotException |
| the slot was declared once and has since been removed | RetiredSlotException — its message tells you to run ui5:sync |
the name is one of tenant, actor, now, at | ReservedSlotNameException (locale is deliberately allowed) |
| the value does not fit the slot's type | InvalidActorParameterValueException |
clear() is the quiet one: clearing a name that has no value, or no slot, deletes nothing and reports success.
A seeder may write SlotAssignment directly — the implementation-consultant path for a company's starting values (The First Seed). It bypasses the writer's checks, so sync first and get the names right.
Reading a value
You do not fetch a slot value. You declare what you need and it is handed to you, already resolved:
class RevenueTile extends AbstractUi5Tile
{
public function getRequiredSlots(): array
{
return [CoreSlots::Currency, CoreSlots::DateFrom, CoreSlots::DateTo];
}
}| Where you are | How the map arrives |
|---|---|
| a tile or chart provider | the first argument of getTile($slots, $context) / getChart($slots, $context) |
| a card | as the query string on the card's manifest URL |
| a report provider | injected into provide(…, array $slots) |
| an SDK action handler | $business->slot('currency') / $business->slots(), after the handler declares SlottableInterface |
Two things this table does not contain, and both surprise people:
- There is no
$context->slot().SdkContextcarries the artifact, tenant, actor, principal, locale, timestamp, abilities and settings — not slots. Slots are resolved per artifact, because what gets resolved depends on what that artifact declared. - Entity sets are not a slot seat. An OData set that needs context reads it from
SdkContextor takes an explicit query option.
For the raw actor layer there is ActorParameterReaderInterface: get('currency') returns the actor's own value or null — no request, no composition, no default. Useful for an administration screen that must show what a person actually set. It is not "the slot value".
Every slot value arrives in the one form its type names — a Date as a Y-m-d string, a DateTime as ISO-8601, a Decimal as a numeric string — whichever source answered (Slots). The reader's get() returns the same form, so an actor's value compares with a resolved one without conversion.
Inspecting the catalog
php artisan ui5:slot # every slot: name, type, default, note, declaring modules
php artisan ui5:slot currency # one slot in detail — and every actor override of itThe second form is the SDK's addition: below the catalog entry it prints a table of every partner who has a value for that slot, with the value and who assigned it. The command is read-only; there is no ui5:slot set.
Why there is a sdk_slots table
The registry already knows every declaration, so the mirror exists for the three things memory cannot do: give sdk_slot_assignments a foreign key to point at, let a query join the catalog against a partner's values in one statement, and give a form request something to validate a slot name against.
ui5:sync keeps it in step, and the registry always wins — type, default, note and grade are overwritten from the declaration. A slot that disappears from the catalog is soft-deleted, never dropped: the assignments that reference it must keep their target. Declare it again and the row is restored.
So: after adding or changing a #[Slot], run ui5:sync. Before that, every write to it fails with the retired-slot error — and if your host runs the cached registry, rebuild that too.
What ships, and what doesn't
Core declares seven slots — currency, locale, timezone, period, date, date_from, date_to — and the SDK declares none of its own. The store, the console and the CLI are exercised end to end; the read path is young, and the first artifact in your product to declare getRequiredSlots() may well be the first anywhere.
Not built, and not promised:
- No per-tenant or per-site values. The actor is the only holder. A tenant default would be another source in the chain.
- No role-derived values. A value comes from the actor, not from what they are.
- No choice validation. A slot's type is checked; whether
CHFis a currency you trade in is your master data's business. - No events and no audit trail. A write records who set it and when, and overwrites the previous value.
- The Settings app does not show slots. Its catalogue excludes them on purpose; the Partners Parameters tab is their surface. The
slot.*rows you may notice insdk_settingsare a by-product of sync and are read by nothing — and the settings writer refuses to override them, so a write there cannot go missing without a word. - The cache is per request. Values are memoised for the request that reads them, no longer.
See also
- Slots: Core's half — declaring a slot, the chain, the sentinels
- Reading & Writing Settings: the other value channel
- The First Seed: where a company's starting values come from
- Mutating Actions: the
#[Act]gate that governs a write