Create a Named Service: Teach the Platform Your Domain
The complete hands-on authoring story: nouns and refs, search vocabulary, role-guarded use cases, honest guards, the presentation layer — and the test that a realm now has two readers: an agent that works it from the schema, and a user who understands and controls it from its service card.
Your team runs a real domain — shipments, contracts, lab samples, campaigns. Agents should work it, users should control it, UIs should open its objects. The usual path is thirty bespoke tools and a custom admin page. The KDCube path is one realm declaration.
The ontologic-tools piece sketched the recipe on a logistics desk; this is the hands-on build — every declaration a realm author makes, in order, and the two readers who consume them.
1. Name the nouns
A realm starts with object kinds and a ref grammar. Refs are identities that round-trip with no ambient session — any surface that holds one can come back to the object.
NAMESPACE = "freight"
SHIPMENT_KIND = "freight.shipment" # freight:shipment:<id>
EXCEPTION_KIND = "freight.exception" # freight:exception:<shipment>:<seq>
POD_KIND = "freight.pod" # freight:pod:<shipment> (proof of delivery)
Each kind gets a one-liner — it teaches both readers later:
OBJECT_KIND_DESCRIPTIONS = {
SHIPMENT_KIND: "One shipment on the desk, with its route and status.",
EXCEPTION_KIND: "One exception raised on a shipment (delay, damage, customs hold).",
POD_KIND: "One proof-of-delivery document attached to a shipment.",
}
2. Write the questions
Search scopes declare what the desk already asks, with self-described filters — the
agent reads these from object_schema and composes queries on the first
try:
FREIGHT_SEARCH_SCOPES = (
NamedServiceSearchScope(
namespace=NAMESPACE,
label="shipments",
object_kind=SHIPMENT_KIND,
description="Search shipments by route, carrier, status, and overdue time.",
filters_schema={
"status": {"enum": ["planned", "in_transit", "delivered", "exception"]},
"carrier": {"type": "string"},
"overdue_days": {"type": "integer", "minimum": 0},
},
),
)
3. Name the use cases
Actions are the realm's bounded verbs — named use cases with declared payloads, never a shell:
"actions": {
"reschedule": {...}, # move the delivery window
"assign_carrier": {...}, # put a carrier on the shipment
"escalate_exception": {...}, # page the on-call desk
"attach_pod": {...}, # attach a proof-of-delivery file
}
They ride the generic grammar (object.action with an action
name), so every operator — the chat agent, an external agent over MCP, a UI drop — calls
them the same way.
4. Guard it
Two honest shapes, depending on whose credentials do the work:
- Internal realm (freight runs on your own store): the door checks
delegated grants (
freight:read,freight:dispatch) per namespace and operation; inside, the realm authorizes with its own rules against the operator's identity. Declare no account claims — nothing is invented. - Provider-backed realm (operations act through a user's external account — the mail/slack shape): declare the connected-account requirements machine-readably, with per-operation differentiation only where it is real.
"connected_accounts": [{
"provider_id": "carrier_portal",
"connector_app_id": "acme",
"provider_label": "Acme Portal",
"claims": ["acme:read", "acme:book"],
"claims_by_operation": {
"object.search": ["acme:read"],
"object.action.assign_carrier": ["acme:book"],
},
"claim_labels": {"acme:read": "read bookings", "acme:book": "book carriers"},
}],
Consent is demand-driven: nothing is asked at turn start. The attempt that hits a missing claim returns the structured answer — reason, labeled candidates, an ABSOLUTE Connection Hub deep link into the consent plan seeded with exactly those claims, a retry hint, and agent-facing instructions — while the turn keeps working.
5. Teach both readers
The schema teaches the agent. The presentation teaches the user — the capability picker renders it as the realm's service card: your entries grouped into Read / Create & update / Actions, your labels forming the group summaries, grammar tokens demoted to expandable details — and it renders nothing you didn't declare:
"presentation": {
"about": "Track shipments, handle exceptions, and dispatch carriers from your desk.",
"works_with": "Works with your freight desk's shipments and their documents.",
# provider-backed realms declare `third_party` instead:
# "Works with your carrier bookings through your connected Acme Portal account."
"operations": {
"object.search": {"label": "Search shipments", "description": "Search shipments by route, carrier, status, and overdue time."},
"object.get": {"label": "Read a shipment", "description": "Read one shipment with its exceptions and documents."},
},
"actions": {
"reschedule": {"label": "Reschedule a shipment", "description": "Move a shipment's delivery window."},
"attach_pod": {"label": "Attach proof of delivery", "description": "Attach a signed delivery document to a shipment."},
},
},
"object_kinds": OBJECT_KIND_DESCRIPTIONS,
Missing text here is a realm defect — the card shows the honest "This service hasn't described itself yet." rather than inventing copy. The user reads the card, understands what the realm does and touches, and narrows it per operation or action; a denied entry is rejected at dispatch, not hidden in UI.
6. Register once
One decorator carries all of it; publication is automatic at bundle load, and every consumer in the tenant/project finds the realm by name:
@named_service_provider(
provider_id="freight.desk",
namespace=NAMESPACE,
refs=("freight:*",),
object_kinds=(SHIPMENT_KIND, EXCEPTION_KIND, POD_KIND),
search_scopes=FREIGHT_SEARCH_SCOPES,
operations=build_default_operations((TRANSPORT_LOCAL, TRANSPORT_API)),
label="Freight desk",
intro="Freight desk — shipments, exceptions, carriers. Search shipments, "
"read one, and run desk actions here.",
description="Freight-desk realm over the team's shipment store.",
metadata={ ...presentation, object_kinds, actions, connected_accounts... },
)
class FreightDeskProvider(NamedServiceProvider):
...
A consumer app declares the namespace and the operations each agent may use
(surfaces.as_consumer.agents.<id>.tools →
kind: named_service); the user's picker narrows within that grant.
7. Test it — both readers
- The agent reader: an operator who has never seen the domain reads
schema, runssearch, follows a ref throughget, performs one guardedaction— correctly, on the first try. - The human reader: a user who has never seen the domain opens the realm's service card and can say what it does, what it works with, and turn any operation or action off.
- The consent path (provider-backed realms): with zero consent, the first attempt returns the structured answer with the seeded deep link; after approval, the same call succeeds.
- Watch the seams: discovery registration logs at bundle load, the
namespace roster in the agent's instructions, the picker's card, and — for external
agents — the same realm through the generic
named_servicesMCP gateway.