Skip to main content
Version: Current

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.objective tells the agent when to use the skill; diagnose what it must understand before offering.
  • invoke names what the skill calls: an operation of the station's APIs (invoke.method), or a capability of Convoy (invoke.native, see below). A skill with kinds and no invoke is refused at load: its items come from a call.
  • kinds names the kinds its rows become, each recognised by its when (see below). Two skills cannot declare the same kind.
  • auto_trigger names 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:

nativewhat Convoy does
searchthe semantic search of the catalogue
routinea routine composed by the bundle engine (bundles)
seta 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. A default that starts with $ is refused. In a playbook whose task binds it (the task's params, see Playbooks), its value is that binding.
  • The default is optional. Without one, only a task's binding sets the parameter: so invoke.args cannot read it (the invoke runs before any submit), and a parameter without default that 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.x that names no declared parameter, a parameter nothing reads, any other $… reference in invoke.args, a parameter without default read by invoke.args. An argument the capability's schema does not take, or a value of another type than its parameter (or outside its enum), refuses the station at assembly: the API would ignore it, coerce it or refuse it.
  • $params.<name> is read in the args of 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_by does not exist yet).

kinds — recognising a kind​

A kind has three keys, all optional:

keyeffect
whenthe condition that says which rows are this kind
selectthis kind's guards (guards only); {} means "no guard"; absent, the skill's
submitthe 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 when is known to hold. An undetermined comparison (a > on an absent value) is not; == and != read an absent value as null ($fields.x != '149' holds for a row without x).
  • Each value is read as its field's declared type: a number in a text or id field is its text (149 is '149'), a number written as text in a numeric field is the number. A literal of another type than its field ($fields.category == 149 on a text field) 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 when takes every row. With several kinds, each declares its when, 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}"}
roletypewhat the card does with it
titletext or idthe line's name. Without a title, the line shows its ref
billedpricethe line's price, in the station's currency (global.currency)
linkurlthe item's page on the tenant's site (see below)
  • One field per role, at most. Two title fields refuse the station at load.
  • Only what is declared is read. Without a billed field, the item has no listed price, even if its rows have a price field. The card's image is the first field of type image.
  • 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: name must be the title, price the billed, image_url the first image. Any other use of these names is refused at load. code, slug and id are refused too: Core writes the item's ref there when it joins the selection.
  • A skill with kinds and no present presents nothing: its rows show their ref.
  • A price's cadence (one_time, monthly…) is read from the row's cadence field.

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.

typewhat Core does
textshows it
email, phonewrites it in one spelling before it identifies a room
number, integerchecks an answer is one (integer: a whole number)
booleanchecks it; in the submit form, a required boolean is a consent, answered only by yes
pricein the station's currency; the billed field is the line's price
quantitya whole number
idan identifier: text or a whole number
urlan address; the link field is the item's page at the tenant
imagethe address of an image; the first one is the card's image
datetimea 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.

writedraws
carousela row of cards you scroll through (articles with images)
cardscards to choose from, which show the skill's form when there is one
checklistan ordered list to tick
reporta figure that answers a question (see below)

The shape of the part​

keyeffect
select.max_items: 1a single item: the choice replaces. The replacement is announced to the person.
depends_on, on_losstied to another skill's part, named by that skill's name. If that part empties: clear empties this one, block blocks the submit.
countable: falsea 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_on and on_loss shape items; written on a skill without kinds, nothing would read them, and Core refuses the station at load.
  • render.report maps the tenant's fields to the "report" frame. Only component: report reads it: with another component, it is refused at load. component: report without render.report is refused too (Verify).
  • yields gives the kind a skill without kinds produces (a routine's articles); presents: selection shows the recap of the selection instead of calling a capability; authenticated: true keeps 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.