Skills
The agent sees only skills. A skill is one declaration: what it is for (cognitive), what
it calls (invoke), and — when its rows are things a person can choose — how they become items
(kinds, present), are reserved (select), executed (submit) and drawn (render). Raw
capabilities are never exposed.
A skill that offers items
A skill that declares kinds takes an operation of the tenant and turns its rows into items a
person can choose. It has a part in the selection, named after it.
skills:
- name: takeDeliveryOption # the skill's name; it is also the name of its part in the selection
cognitive: # rendered into the tool description
objective: "offer pickup or delivery"
diagnose: [the customer's address, the time they want]
invoke: {method: getDeliveryOptions} # the operation that returns the rows
kinds: # the kinds of these items, each recognised by its `when`
pickup:
when: "$fields.type == 'pickup'"
select: {} # what changes for this kind: {} means "no guard"
submit: [{name: collect, mode: self}]
delivery:
when: "$fields.type == 'delivery'" # keeps the skill's select and submit
present: # what the items carry: typed fields (see below)
fields:
name: {from: name, type: text, role: title}
type: {from: type, type: text} # what tells the two kinds apart
render: {component: cards} # how to show them
depends_on: takeMealSelection # no delivery for an empty selection…
on_loss: clear # …and it goes with the last dish (or `block`)
select:
max_items: 1 # only one: choosing another replaces the first
guards: [...] # see "Policies"
submit: [...]
auto_trigger: take-meal-order # using it opens this playbook
cognitive.objectivetells the agent when to use the skill;diagnosewhat it must understand before offering.invokenames what the skill calls: an operation of the station's APIs (invoke.method), or a capability of Convoy (invoke.native, see below). A skill withkindsand noinvokeis refused at load: its items come from a call.kindsnames the kinds its rows become, each recognised by itswhen(see below). Two skills cannot declare the same kind.auto_triggernames the playbook the skill opens when it is used (Playbooks).
invoke.native — Convoy's capabilities
invoke.method names only an operation of the station's APIs. Convoy's own capabilities are named
with invoke.native:
native | what Convoy does |
|---|---|
search | the semantic search of the catalogue |
routine | a routine composed by the bundle engine (bundles) |
set | a set composed on request, one article per element asked for |
- name: findProducts
invoke: {native: search}
kinds: {product: {}}
A skill names one capability: method or native, never both. method: search_products (or
build_routine, build_set) is refused at load, with the native spelling as the remedy.
params and invoke.args — the arguments the station fixes
invoke.args fixes arguments of the call. Each is a literal value or $params.<name>, a parameter
of the skill declared in params:
- name: findDrinks
params:
category: {default: "149"}
invoke:
method: getMenu
args: {category: "$params.category"}
- A parameter's value is its
default, a literal: it is read before the call, when no item exists yet. Adefaultthat starts with$is refused. In a playbook whose task binds it (the task'sparams, see Playbooks), its value is that binding. - The
defaultis optional. Without one, only a task's binding sets the parameter: soinvoke.argscannot read it (the invoke runs before any submit), and a parameter withoutdefaultthat no task binds is refused at load. - A fixed argument is sent on every call of the skill's capability — its items, the catalogue's
live search and seed, a skill without
kinds— over any value the model sent. It is removed from the input schema both doors (MCP, agent) advertise for the skill: the model is never asked for it. - Refused at load: a
$params.xthat names no declared parameter, a parameter nothing reads, any other$…reference ininvoke.args, a parameter withoutdefaultread byinvoke.args. An argument the capability's schema does not take, or a value of another type than its parameter (or outside itsenum), refuses the station at assembly: the API would ignore it, coerce it or refuse it. $params.<name>is read in theargsof the skill's submit calls too (its own and its kinds'): The submit. A guard does not read it; the model does not set a parameter (set_bydoes not exist yet).
kinds — recognising a kind
A kind has three keys, all optional:
| key | effect |
|---|---|
when | the condition that says which rows are this kind |
select | this kind's guards (guards only); {} means "no guard"; absent, the skill's |
submit | the call that files this kind's items (one); absent, the skill's |
when is read on the row, before it is an item. It is a condition in the same language as the
guards (Policies), but it reads only $fields.<name>, a field
declared in the skill's present.fields. Any other reference ($item…, $params…, a row key not
presented) is refused at load. A row's kind or type key is never read on its own: to use it,
present it.
- A row is of a kind when its
whenis known to hold. An undetermined comparison (a>on an absent value) is not;==and!=read an absent value asnull($fields.x != '149'holds for a row withoutx). - Each value is read as its field's declared type: a number in a
textoridfield is its text (149is'149'), a number written as text in a numeric field is the number. A literal of another type than its field ($fields.category == 149on atextfield) is refused at load: it could never match. - One kind holds: the row is that kind. None, or several: the row is not offered (it is logged).
- A single kind without
whentakes every row. With several kinds, each declares itswhen, or the station is refused.
When one operation returns items that are not handled the same way: at a restaurant, pickup needs
no address, costs nothing and sends no courier (the example at the top of the page). A single
kind's when filters rows: one operation can feed a dishes skill and a drinks skill.
- name: takeMealSelection
invoke: {method: getMenu}
kinds: {dish: {when: "$fields.category != '149'"}}
present:
fields:
name: {from: name, type: text, role: title}
category: {from: category, type: text}
- name: takeDrinkSelection
invoke: {method: getMenu}
kinds: {drink: {when: "$fields.category == '149' and $fields.price > 0"}}
present:
fields:
name: {from: name, type: text, role: title}
price: {from: price, type: price, role: billed}
category: {from: category, type: text}
present — typed fields
present.fields declares what the skill's items carry. Each field has a name chosen by the
station, a source (from, the field of the tenant's row; a dotted path, prix.ttc, reads inside
an object), a type and sometimes a role:
present:
fields:
name: {from: name, type: text, role: title}
price: {from: price, type: price, role: billed}
image: {from: image_url, type: image}
page: {from: slug, type: url, role: link,
template: "{tenant_url}/article/{value}"}
| role | type | what the card does with it |
|---|---|---|
title | text or id | the line's name. Without a title, the line shows its ref |
billed | price | the line's price, in the station's currency (global.currency) |
link | url | the item's page on the tenant's site (see below) |
- One field per role, at most. Two
titlefields refuse the station at load. - Only what is declared is read. Without a
billedfield, the item has no listed price, even if its rows have apricefield. The card's image is the first field of typeimage. - Every field is on the item under its own name:
$item.fields.<name>in a submit, beside the tenant's row, and in a condition (require,when). - Three names are reserved:
namemust be thetitle,pricethebilled,image_urlthe firstimage. Any other use of these names is refused at load.code,slugandidare refused too: Core writes the item's ref there when it joins the selection. - A skill with
kindsand nopresentpresents nothing: its rows show their ref. - A price's cadence (
one_time,monthly…) is read from the row'scadencefield.
The link to the tenant's site. The role: link field gives the item's page. Its template has
only two placeholders: {value}, the field's value (encoded so it stays inside its path segment),
and {tenant_url}, the deployment's TENANT_URL (a prefix). A template without {value}, or
with any other placeholder, is refused at load; so is a template that starts with neither
{tenant_url} nor http(s):// (it would resolve against the host that draws the card), and a
template on any field but the link. The value is read on the item, under the link field's name.
Without a template, the value itself is the link, only when it is an absolute http(s) address.
Without a link field, the card has no link: Convoy never invents an address on the tenant's
domain. The catalogue search and the routine take the link of the catalogue's skill
(catalog.from_skill).
A catalogue row — resolved by its ref alone (a routine's article, a ref never shown in the room)
or read by a host — is read through the present of the skill that reads the catalogue: the one
catalog.from_skill names, else the station's native search skill (invoke: {native: search}).
Its title, billed and first image fields give the row's name, price and image. With no such
skill, a ref the room was never shown is unknown: nothing says what it is called or costs. A
catalogue skill without a billed field is said at load (its rows have no price). Two native search
skills that read a row's title, price or image from different fields are refused at load.
Field types
A present field and a form field (the form guard of select.guards or of kinds.<k>.select.guards,
submit_guard.form) take their type from the same table. A type outside the table refuses the
station at load.
| type | what Core does |
|---|---|
text | shows it |
email, phone | writes it in one spelling before it identifies a room |
number, integer | checks an answer is one (integer: a whole number) |
boolean | checks it; in the submit form, a required boolean is a consent, answered only by yes |
price | in the station's currency; the billed field is the line's price |
quantity | a whole number |
id | an identifier: text or a whole number |
url | an address; the link field is the item's page at the tenant |
image | the address of an image; the first one is the card's image |
datetime | a date, or a date and a time, as ISO 8601 text |
In a form, an answer is checked by its type, and a boolean is never a number. The model is told a
price is "a number" and a quantity "a whole number"; the surfaces draw number, integer,
price and quantity as number inputs. Only text, email and phone can identify a room
(Auth).
render — how the result is drawn
render.component takes a name from a closed vocabulary; an unknown name makes the station refuse
to start. Without render, the items of a skill with kinds are cards; for a skill without
kinds, its capability's result decides.
| write | draws |
|---|---|
carousel | a row of cards you scroll through (articles with images) |
cards | cards to choose from, which show the skill's form when there is one |
checklist | an ordered list to tick |
report | a figure that answers a question (see below) |
The shape of the part
| key | effect |
|---|---|
select.max_items: 1 | a single item: the choice replaces. The replacement is announced to the person. |
depends_on, on_loss | tied to another skill's part, named by that skill's name. If that part empties: clear empties this one, block blocks the submit. |
countable: false | a set, not quantities: each item once (timesheets, time slots). No more −/+ button, and any quantity request is refused. |
A skill that answers
A skill without kinds answers with what it calls: a read, a report, a routine. Its rows are
not items.
skills:
- name: rapport-heures-sup
cognitive:
objective: "answer about a team's overtime for a month"
invoke: {method: getOvertimeReport} # declared access: read
render:
component: report
report:
headline: total_overtime_hours
headline_label: "overtime hours"
unit: h
series: weeks
point_label: week_starting
point_value: overtime_hours
highlight: worst_week # marks the point; never re-sorts
- Item keys are refused without
kinds.present,select,submit,countable,depends_onandon_lossshape items; written on a skill withoutkinds, nothing would read them, and Core refuses the station at load. render.reportmaps the tenant's fields to the "report" frame. Onlycomponent: reportreads it: with another component, it is refused at load.component: reportwithoutrender.reportis refused too (Verify).yieldsgives the kind a skill withoutkindsproduces (a routine's articles);presents: selectionshows the recap of the selection instead of calling a capability;authenticated: truekeeps the skill for a signed-in person (Auth).
A skill that only answers
It is not declared: Core deduces it. A skill is a read when what it calls (its invoke) is an
operation declared access: read, and nothing it shows can be filed: no submit, no guard
(select.guards empty), no other skill that pulls its kind into a submit or depends on it
(depends_on), no yields and no presents. A native capability (invoke: {native: …}) has no
declared access: it never counts as a read. There is no read_only key;
written, it is refused.
It is a Core guarantee: the rows of a read skill are never kept as offered, so no door can add
them to the selection. Core also refuses an invoke that names an access: write operation: the
write would go out with no submit and no confirmation.