Injection zones
Register a widget with ConsoleExtension.widgets: [{ slot, widgetId, title?, Component }]; the shell renders it through PluginWidgetSlot.
The guaranteed zones are the exported CONSOLE_WIDGET_SLOTS catalog —
slot stays an open string so custom zones don't need a core release.
| Zone | Where it renders | Props your widget receives |
|---|---|---|
hostActivity | Host dashboard + a page's detail view: the activity column | hostId, targetId?, header?, viewAllHref? |
hostDashboard | Host dashboard glance row, one card per capability | hostId |
orgDashboard | The organization's Sites page, above the site grid — the org-level twin of hostDashboard, rendered only for an org-wide member who may open the org-level CRM | hostId (always null), orgMount, basePath (the org-level hub's path) |
commerceGlance | Host dashboard commerce summary | hostId |
hostAnalytics | A site's Analytics page, below its traffic cards: a whole section a plugin computes, such as Funnels | hostId, orgId |
orgData | Organization → Data page body | orgId, org |
besignerFunctions | Besigner ƒx panel | hostId |
hostArtifactPublish | Wherever a console page offers to publish something it holds (a site's layouts, the organization's publish panel): the dialog that publishes it. The page keeps the control that opens it, and leaves that control out when no widget is registered here | artifact ({ kind, hostId?, orgId?, artifactId?, displayName?, description? }, or null while nothing is open), onClose() |
orgPluginInstalls | Organization → Plugins, above the built-in plugins: the plugins your plugin installed into the workspace, one row per installation, each linking to /[orgSlug]/plugins/[pluginRef] | orgId, orgSlug, hosts ({ id, label } for each site the reader can see) |
pluginInstallStatus | An installation's own page, above where it runs: what the installing plugin says about the version the workspace runs. Drawn only for an installation that exists | orgSlug, pluginRef (the installation's id), pin (one pin of it) |
templateGallery | The template gallery ("Start from a template" on a site's Pages, Layouts and Components tabs), below the site's own templates and the starters: a shelf of templates your plugin offers to install. Call reportShelf with loading, empty or shown so the gallery's "nothing matches" line counts your shelf, and onInstalled() once an install lands so the gallery closes | hostId, kind (page, layout or component), search (the word typed in the gallery's search, '' for none), onInstalled(), reportShelf(shelfId, state) |
templateInstallStatus | A row of a site's Templates library whose template a plugin installed, beside its Source badge: what your plugin says about the copy the site holds, such as an update to install. Drawn once per such row; draw nothing for a template you did not install | hostId, template (the row's template document, $id included) |
sitePackageItemPreview | One side of an item in a site package import's Changes step, beside the other side: the item drawn the way your plugin previews it. Name the package kinds your widget draws in the widget's itemKinds (for example itemKinds: ['form']); the import draws your widget for items of those kinds only, once for the site's copy and once for the file's, and a kind no widget names keeps the console's own rendering or its value list. Your widget reads what its preview always reads and writes nothing. The console asks your widget's own permission here, not your extension's: the import's card already admits whoever may import the package, and your extension's permission guards your own pages | hostId, side ('site' or 'file'), itemKey (<kind>/<id>), kind, itemId, content (the item as the import would write it, without $id), title — ConsoleSitePackageItemPreviewZoneProps |
dashboardFooter | Bottom of the host dashboard | hostId |
orgSettings | Organization → Settings, below the tabs | orgId, org |
hostSettings | Host setup page, below the built-in cards | hostId |
hostSeo | A site's Setup → SEO, under the SEO check and above the SEO cards: fixes for what the check found, and values proposed for the cards, which each card's Update writes | hostId, orgId, orgSlug, host, seo (the stored settings), check (the SEO check's last report and the keyword lines it ran with; null until someone runs it — add to its findings, never list them again), proposeDraft(values, key) — puts values in the SEO cards as unsaved edits |
seoFields | Inside a search listing editor, under its fields: a page's SEO card, and the commerce product editor's search engine listing | hostId, orgId, orgSlug, subject (the page or the product), fields, values, hasImage, proposeValues(values, key) — stages values in the editor as unsaved edits |
themeEditorFonts | Inside the theme editor's Typography card on a site's Setup → Theme: the control that chooses the site's fonts. Your widget edits the editor's draft and never saves: the editor's Save keeps the change and Discard drops it. With no widget here the editor offers its own short list of fonts | hostId (null on an editor that names no site), draft (the theme with every unsaved edit on it), updateDraft(updater) — changes the draft as the editor's own controls do — ConsoleThemeEditorFontsZoneProps |
adminOrgDetail | Staff admin org detail page (staff-only) | orgId |
orgBillingUsage | Billing → Usage, below the meters | orgId, org (the billing-merged org doc), canManage |
orgBillingOverview | Billing → Overview, among the plan and add-on cards | orgId, org, plan, canManage |
staffOrg | Staff org page, among its cards (staff-only) | orgId |
staffUser | Staff user page, below the account's activity (staff-only) | uid |
staffSite | Staff site page, below its own cards (staff-only) | hostId, orgId ('' for none), host (the site document, undefined while loading) |
staffOrgsListColumn | A column of the staff Organizations list — see Column zones (staff-only) | per row: row, orgId, orgIds (every org on the page); its Header: orgIds |
staffOrgUsageColumn | The staff org usage table: a column between Forms and Cost when the widget declares column, a line above the table otherwise (staff-only) | per month: month, orgId; above the table: orgId, org (the org document, where the page holds one) |
orgMember | Team → member detail, below the member's activity | orgId, uid, member, canManage |
orgMembersListColumn | A column of the org Team table — see Column zones | per row: member, orgId, canManage |
hostMembers | The site collaborators card: a column of its table when the widget declares column, a card beneath it otherwise | per row: member, hostId, canManage; as a card: hostId, canManage |
siteMember | A visitor account's drawer on a site's Users page, between the account's password help and its saved addresses: what your plugin holds about the person — what they bought, what they subscribe to. Each widget is a section of the drawer's column; open it with a Divider heading like the drawer's own | hostId, member (the account's siteMembers document, $id included; its email is how to find what the person did on the site) |
consoleDock | The console dock: a floating panel above every route boundary in both the app and editor shells (it was assistPanel until AGL-3080) | orgId, org, orgReady, scopedOrgId (the org a widget may act and be metered for, undefined where the page names none), orgSlug, hostId, productName, releaseVerdict(key) ({ visible, staffPreview } for any release flag, staff bypass applied), isStaff, permissionsOnHost |
consoleTopBar | The console's top bar, among its status controls just ahead of the notifications bell, on every page that draws the bar. One small control that keeps work in progress, or something waiting on the reader, in view; it draws nothing when it has nothing to say | The same props as consoleDock |
besignerInspector | A section at the bottom of the besigner's Attributes panel, under the selected element's fields, on every editor the designer opens | hostId (null on an editor that names no site), node (the selected element) |
besignerToolbar | The besigner's secondary toolbar, after undo and redo, on every editor the designer opens | hostId (null on an editor that names no site) |
besignerInteractions | The besigner's Interactions section, on every editor that offers one. Your widget draws nothing: it reads the section experiments your plugin runs on the site and calls reportSectionExperiments from an effect, and the section badges an element that has one and offers to start one from your create. Report null to withdraw | hostId, screenId (null on a layout or a component, which is no page to run one on: report no create there), reportSectionExperiments(reporterId, { experiments, create? } | null) |
besignerPageProperties | A section at the foot of the Besigner's Page Properties drawer, under the page's publishing, layout, SEO and password sections: what your plugin makes of the page itself, such as serving it once per record. Your widget saves through its own routes, never the drawer's buttons | hostId, orgId (undefined while it resolves), screenId (the page in the editor), screenKind? (the page's stored kind: 'template' for a template, absent for a page) — ConsoleBesignerPagePropertiesZoneProps |
hostScreenRow | Inside each row of a site's Pages list, beside the page's name: a chip about that page, such as that it is a record template and how many pages it serves. Drawn once per row, so read what you need once for the site and answer each row from that; draw nothing for a page you have nothing to say about | hostId, orgId (undefined while it resolves), screenId (the row's page), screenKind? — ConsoleHostScreenRowZoneProps |
hostScreens | A site's Pages list, beside Templates and Create New Page: another way to start a page | hostId, orgId (undefined while the page resolves it) |
hostTemplates | A site's Templates page, beside Create Template: another way to start a template | hostId, orgId |
hostLayouts | A site's Layouts page, beside Templates and Create New Layout: another way to start a layout | hostId, orgId |
hostComponents | A site's Components page, beside Templates and Create Component: another way to start a reusable component | hostId, orgId |
mediaLibrary | The media library, beside Upload media and New folder, and again in an empty library's call to action: another way to add a file. Not drawn in a picker narrowed to video or PDFs | hostId (the site whose library is open; for the organization library, the site on screen, else null), orgId, library ('host' or 'org'), folderId (the open folder, where new files land, or null), onCreated(mediaIds) (the library refreshes and selects the assets the widget added) |
recordInsights | A CRM contact's, company's, deal's or lead's page, under its header. Hosted by the CRM plugin (see Zones a plugin hosts) | hostId (null at the organization level), orgId, record ({ kind, id, name }), proposeTask(task, key) (opens the CRM's task form filled in; absent on a lead), and on a deal stages, stageId and proposeStage(stageId, key) (asks, then moves the deal through its stage route) |
recordEmail | Inside the CRM's one-to-one composer, under the message. Hosted by the CRM plugin (see Zones a plugin hosts) | hostId, orgId, record, subject, body, proposeDraft({ subject, body }, key) (fills the composer, asking before it replaces a written message; Send is the member's) |
importMapping | Under the column matching of a CRM import drawer, and of the import wizard's Columns step on any surface that names the zone (see Import and export). Hosted by the CRM plugin and the wizard (see Zones a plugin hosts) | hostId, orgId, collection, columns (each { header, shape }, where shape is email, phone, number, date, yes-no, url, text or empty; never a cell), mapping, proposeMapping(mapping, key) (replaces the drawer's matching; Import is the write) |
Rules of thumb: widgets receive shell-resolved context as props and must
not reach for console-app hooks; data access goes through
@aglyn/tenant-feature-instance (useFirestoreCollection, useUser,
usePluginConfig, …). A widget renders for a workspace only when its
plugin is enabled and released — the shell never mounts widgets from
unloaded plugins.
Zones a plugin hosts
A zone can sit on a plugin's own surface rather than on a console page, such as hostForms
on the forms plugin's Forms page, hostEmailTemplates on the email plugin's templates
list, hostCampaigns on the marketing plugin's Campaigns, hostAutomations, automationEditor and automationRun
on the workflows plugin's Automation page, orgAutomations on its Org automations section, hostLogic, logicFunctionEditor and
logicReferenceIssue on the logic plugin's Functions & Variables page, or recordInsights, recordEmail and
importMapping on the CRM plugin's record pages, one-to-one composer and import drawers, and in the import wizard. A plugin cannot import the console's PluginWidgetSlot,
so the shell hands its renderer down: read it with useConsoleWidgetSlot() from
@aglyn/aglyn and draw the zone through it.
const Slot = useConsoleWidgetSlot()
return Slot ? <Slot slot={HOST_FORMS_ZONE.id} hostId={hostId} orgId={orgId} /> : null
The renderer is the same gated slot a console page mounts, so a widget there passes the
same enablement, entitlement and permission gates. Outside the console shell it is null,
and the zone draws nothing.
A plugin that hosts a zone also declares it, with registerPluginZone and a token that
carries the props it hands each widget (see
Zones a plugin hosts in the
plugin-manager reference). The forms, email, marketing, workflows and commerce plugins declare these on
their own surfaces; a widget from another plugin restates the props it reads rather than
importing the host's package:
| Zone | Where it renders | Props your widget receives |
|---|---|---|
hostForms | A site's Forms page, the forms plugin's, beside Create Form: another way to start a form | hostId, orgId |
hostEmailTemplates | A site's email templates, the email plugin's, beside New template and in the empty list: another way to start an email design | hostId, orgId |
hostAutomations | The workflows plugin's Automation page: on Actions, beside Add action and Recipes; on Workflows, in the card's header, or its empty state while it has none. Another way to start an automation | hostId, orgId, openAction(actionId) — opens a listed action in the Actions editor (from Workflows, by going to Actions with the action named), and answers false for one the list has not read yet |
orgAutomations | The workflows plugin's workspace Org automations section, in the card's header, for a member who may write org automations. Another way to start one | orgId, triggers and steps — the host events an org automation may start on and the step types it may hold, as that plugin lists them — and propose(automation), which opens { name, trigger, steps } in the section's editor as a new automation, unsaved and switched off, and answers false when the plan lacks org automations or the automation is not one the section can hold |
automationEditor | Inside the editor of one saved automation, an action or a workflow, on the Automation page | hostId, orgId, target ({ type: 'action' | 'workflow', id, name }, the automation as it is stored), and in an action's editor openAction(actionId) — opens another listed action, such as a copy the widget drafted, in its place |
automationRun | On each failed run in an automation's run history | hostId, orgId, target (as above), runId (the run's entry in the site's activity log) |
hostLogic | The logic plugin's Functions & Variables page, in the header of the Functions card and of the Variables card: another way to start one | hostId, orgId, kind ('function' or 'variable', the card it is drawn on), propose(proposal) — opens a proposal of that kind in the card's editor, unsaved, and answers false when it cannot (the plan's cap reached) |
logicFunctionEditor | Inside the editor of one saved function, on the Functions & Variables page | hostId, orgId, target ({ id, name }, the function as it is stored), propose(proposal) — replaces what the editor holds, unsaved |
logicReferenceIssue | On each broken reference the Reference health card lists | hostId, orgId, issue ({ source, sourceId, sourceName, refType, missing }) |
productEditor | The commerce product editor, under a product's description, tags and categories: copy proposed for the fields, which Save product writes | hostId, orgId, product (as the editor holds it), categories, proposeValues(values, key) — stages copy in the editor as unsaved edits |
productsHub | The commerce products page, above its catalog table: proposals the hub writes when a member applies them | hostId, orgId, products (the catalog rows the hub holds), lastImport (the products the latest import created, with its options, or null), and the hub's writes a widget asks for: applyProductCopy, createProductDrafts, createCategories, createDiscountDrafts |
productsCreate | The commerce products page, beside Add product and in the empty catalog: another way to start a product | hostId, orgId (undefined: the page does not know the org; read it from the site) |
productImport | The commerce products import wizard's After import step: options for what happens to the imported products once they land | hostId, orgId, count (products the dry run creates), options, setOption(key, on) |
orderDetail | The commerce order dialog, above its actions: a widget that reads the order and records a shipment, such as buying a shipping label | hostId, orgId, order (id, number, status, currency, the buyer, shippingAddress, lines with each line's fulfilledQuantity, remainingQuantity and requiresShipping, fulfillments, totals, testMode), recordFulfillment({ lineItems?, carrier, trackingNumber, trackingUrl?, labelUrl?, notify?, idempotencyKey }) — records a shipment through the dialog's own route and resolves with it |
orderFulfillment | Inside the order dialog's Fulfill items panel: a widget that fills in the carrier and tracking for the units picked | everything orderDetail hands, plus selection (the { lineItemId, quantity } units picked in the panel) and applyTracking({ carrier, trackingNumber, trackingUrl?, labelUrl? }) — fills the panel's fields for the merchant to confirm with Fulfill |
returnDetail | The commerce return dialog, above its actions: a widget that buys the buyer a return label | hostId, orgId, return (id, status, orderId, orderNumber, the buyer, lines with name, quantity and reason, fromAddress — the order's ship-to, where the parcel comes from — and returnLabel or null), attachReturnLabel({ carrier, trackingNumber, labelUrl, trackingUrl? }) — attaches the label through the plugin's own route, which emails it to the buyer when the return is already approved |
hostOverlays | A site's Marketing → Overlays section, the marketing plugin's, beside New bar and New popup and again in the empty list: another way to start an overlay | hostId, limits (the longest each copy field may be), triggers (the popup triggers, each with its unit and range), createOverlayDraft(kind, proposal) — writes the overlay switched off, cut to the limits, and opens it in the editor |
hostCampaigns | The marketing plugin's Campaigns section, a site's or the organization's, beside Create campaign and again in the empty list: another way to start a campaign | hostId (null on the organization's hub), orgId, sites (on the organization's hub, the sites a campaign could be placed on, each { id, name }; empty under a site) |
marketingInsights | In the header of the marketing plugin's Conversions section and of one campaign's report, beside the page's own actions: the figures in words | hostId (the site the figures are one site's: the site picked, or the site a campaign email was sent as; null when there is none yet), subject (conversions | campaign), campaign (the campaign's subject line on its report, else null) |
overlayEditor | Among the fields of the marketing plugin's overlay editor: copy proposed for the bar or popup being edited, which the editor's Save writes | hostId, overlayId (empty while new), kind (bar | popup), copy (the copy as the editor holds it), limits, triggers, proposeValues(proposal, key) — fills the fields unsaved |
funnelsCreate | The funnels plugin's Funnels card on a site's Analytics page, beside New funnel and in its empty state: another way to start a funnel | hostId, orgId, propose(brief) — asks the funnels plugin for a draft checked against the site and opens the editor on it; resolves to null, or a sentence saying why there is no draft |
funnelInsight | Under a funnel's results on the Funnels card: a control that explains them | hostId, orgId, funnelName, days (the range shown) |
How a zone spaces your widget
Most zones are a stack. The shell draws their widgets one under another, with the same gap the page puts between its own cards, and keeps that gap between the zone and the page's cards beside it. Render your card with no outer margin: the zone spaces it, and a margin on your widget's root is reset.
The other zones hand each widget to a layout the page draws itself, and the page spaces it there:
hostDashboard,commerceGlanceandorgDashboard: a tile of a dashboard grid.hostScreens,hostTemplates,hostLayouts,hostForms,hostEmailTemplates,productsCreate,hostComponents,mediaLibraryandbesignerToolbar: a control in a row.hostAutomations,automationEditorandautomationRun: a control the workflows plugin places beside its Actions buttons and in its Workflows card's header, in an automation's editor, and on a failed run.orgAutomations: a control the workflows plugin places in its Org automations card's header.hostLogic,logicFunctionEditorandlogicReferenceIssue: a control the logic plugin places in its Functions and Variables cards' headers, in a function's editor, and on a broken reference.funnelsCreateandfunnelInsight: a control the funnels plugin places beside New funnel and under a funnel's results.siteMember: a section of a site user's drawer.besignerInspectorandseoFields: a section among a panel's own fields.besignerPageProperties: a section of the Page Properties drawer's column.hostScreenRow: a chip in a Pages list row, beside the page's own chips.productEditor,productsHubandproductImport: a section the commerce plugin places among its product editor's fields, above its catalog table, and in its import wizard's After import step.orderDetailandorderFulfillment: a section the commerce plugin places in its order dialog, above the actions and inside the Fulfill items panel.returnDetail: a section the commerce plugin places in its return dialog, above the actions.hostOverlays,hostCampaigns,marketingInsightsandoverlayEditor: a control the marketing plugin places beside its New bar and New popup buttons, beside Create campaign and in a report's header, and a section among its overlay editor's fields.recordEmailandimportMapping: a section the CRM plugin places under its one-to-one composer's message and under an import drawer's or the import wizard's column matching.besignerFunctions,orgData,orgMarketplace,orgAddonsandmarketplaceListing: the body of a dialog or a page.consoleDock: a floating dock.consoleTopBar: a control in the top bar's row.besignerInteractions: nothing; a widget there reports to the section and rendersnull.orgMembersListColumn,staffOrgsListColumnandstaffOrgUsageColumn: a column of a table, or, onstaffOrgUsageColumn, a line above it.
A widget that renders nothing leaves no gap in either kind of zone.
Staff zones
adminOrgDetail, staffOrg, staffUser, staffSite, staffOrgsListColumn
and staffOrgUsageColumn are on the staff pages, which
name no workspace: a staff page is about an org, a site or an account, not about the
reader's own. So these zones do not read an org's enabled plugins. The staff
area loads every plugin whose plugins.config.json entry names a staff
register surface (see the manifest), before any
staff page renders, and a staff zone renders those plugins' widgets. A
widget's featureFlag and permission are not consulted there: both are
answers about a workspace, and the staff area's guard admits the reader.
A plugin with a widget on a staff zone and no staff surface is never
loaded on the staff pages, so its widget never renders.
Column zones
A zone documented as a column (orgMembersListColumn and
staffOrgsListColumn, and hostMembers and staffOrgUsageColumn when you
want a column rather than a card) takes a widget with a column:
widgets: [
{
slot: 'orgMembersListColumn',
widgetId: 'ai-usage-column',
column: { header: 'AI this month', sortKey: 'aiCredits', align: 'right' },
Component: AiUsageCell, // rendered once per row with { member, orgId, canManage }
},
]
The table draws the header and mounts your component once per row with the
row beside the zone's props; sortKey names the row field a sortable table
orders by (the two member tables render in fetch order today and carry it
for the ones that will). A widget on a column zone without a column is not
a column and renders nothing there — register a card on a card zone instead.
On a zone that takes both, a column widget is drawn only in the table, never
among the cards.
widgetId is a persisted identifier
On the host dashboard a person chooses which cards they keep and in what
order, and that choice is stored by widgetId. Give a retired id to a
different card and a returning reader gets an arrangement they never made
— a card they never hid, hidden. Retire an id by leaving it reserved and
minting a new one; never reuse it.
Give every dashboard widget a title, matching the heading the card
itself renders. It is the name beside the switch that controls the card,
and the two sit a click apart. Without one the shell falls back to the
extension's displayName, which reads correctly for a plugin
contributing one card and ambiguously for one contributing several.
The reader's choice is applied strictly after enablement and entitlement and can only subtract: a widget the workspace is not entitled to stays absent however the stored preference is written, and a widget no stored preference mentions renders. Nothing a plugin declares participates in that decision.