Product · medium — a restaurant
The case. You choose dishes, then pickup at the counter or delivery. Delivery asks for an address, checks that delivery is possible, computes a price. Pickup asks for nothing.
What the example shows. Two skills with kinds, a dependency between them, two kinds recognised
by their when, one handled separately (kinds.pickup), a playbook whose second task is a choice (one_of: pickup or delivery), idempotency, a confirmation before any
submit, and a phone number that identifies the room and is then proved with a code.
examples/actuelle/product-medium.yaml
# Product · medium — a restaurant: dishes, then pickup OR delivery.
# Two skills, one dependency (no delivery for nothing), one kind that differs (pickup is free and
# asks nothing), one playbook whose second task is a choice — pickup or delivery — a confirmation
# before anything is filed.
station:
id: teranga
name: Teranga Kitchen
identity:
providers:
sms: {mechanism: otp, verifies: phone, via: tenant, send: sendOtp, check: checkOtp}
global:
language: fr
currency: XOF
locale: fr-SN
domain:
domain_type: restaurant
entity_name: plat
entity_name_plural: plats
agent:
tone: friendly
name: Teranga
persona: "un hôte chaleureux qui aide à composer le repas"
rules:
- "Ne jamais inventer un plat : uniquement ce que le menu renvoie."
- "Les prix sont en francs CFA (ex : 8 500 F CFA)."
- "Nous livrons à Dakar uniquement ; ailleurs, proposer le retrait au comptoir."
portals:
web:
widget:
origins: [https://teranga.example.sn]
apis:
- name: cuisine
openapi_url: https://api.teranga.example.sn/openapi.json
base_url: https://api.teranga.example.sn
operations:
- {id: getMenu, access: read}
- {id: placeOrder, access: write}
- {id: getDeliveryOptions, access: read}
- {id: checkDeliverable, access: read}
- {id: getDeliveryPrice, access: read}
- {id: createDelivery, access: write, reversible: false}
- {id: sendOtp, access: write}
- {id: checkOtp, access: write}
auth: {type: api_key, key_name: X-API-Key, key_location: header, key_env: TERANGA_KEY}
skills:
- name: takeMealSelection
auto_trigger: take-meal-order
cognitive:
objective: "montrer la carte pour choisir les plats"
diagnose: [l'envie du moment, les restrictions alimentaires]
invoke: {method: getMenu}
kinds: {dish: {}}
present:
fields:
name: {from: name, type: text, role: title}
price: {from: price, type: price, role: billed}
image: {from: image_url, type: image}
submit:
- name: order
mode: request
tool: placeOrder
args:
body.items: "$selection[kind=dish]"
body.idempotency_key: "$action.key" # a retry never orders twice
line_fields: {ref: slug, quantity: quantity}
render: {component: carousel}
- name: takeDeliveryOption
cognitive:
objective: "proposer le retrait ou la livraison"
invoke: {method: getDeliveryOptions}
kinds: # told by the row's `type`, a presented field
pickup:
when: "$fields.type == 'pickup'"
select: {} # no address, no check, no fee
submit: [{name: collect, mode: self}] # collected at the counter: filed, nothing dispatched
delivery:
when: "$fields.type == 'delivery'"
depends_on: takeMealSelection # no delivery for an empty order…
on_loss: clear # …and it leaves with the last dish
present:
fields:
name: {from: name, type: text, role: title}
type: {from: type, type: text}
select:
max_items: 1 # one option: choosing delivery replaces pickup
guards:
- form:
address: {type: text, required: true, label: "Adresse de livraison"}
phone: {type: phone, required: true, label: "Téléphone du livreur", identifies: true}
- check: checkDeliverable
- update: {price: getDeliveryPrice}
submit:
- name: dispatch
mode: request
tool: createDelivery
args:
body.address: "$item.fields.address"
body.phone: "$item.fields.phone"
body.items: "$selection[kind=dish]"
body.fee: "$item.price" # the amount the guard quoted
render: {component: cards}
playbooks:
- name: take-meal-order
goal: "commander un repas : les plats, puis le retrait ou la livraison"
tasks:
plats: {skill: takeMealSelection, label: "Vos plats"}
remise:
label: "Retrait ou livraison"
after: [plats]
one_of: # two methods, one skill: each is one of its kinds
retrait: {skill: takeDeliveryOption, kind: pickup}
livraison: {skill: takeDeliveryOption, kind: delivery}
choose: customer # the person picks (the default)
catalog:
from_skill: takeMealSelection
mode: live
submit_guard:
confirm: true
What happens
| step | Core | result |
|---|---|---|
| the agent shows the menu | auto_trigger opens take-meal-order | the journey shows: "Your dishes", "Pickup or delivery" |
| a yassa added | task "Your dishes" done | total 8 500 F CFA |
| "Finalise" | task "Pickup or delivery" (remise) required, empty | refused: "Retrait ou livraison : étape requise de cette commande — ajoutez d'abord pickup ou delivery" (the station speaks French) |
| pickup chosen | the row has type: pickup, so kinds.pickup, the retrait method: no form, no fee | ready |
| delivery chosen instead | select.max_items: 1; step_open checks the selection without the pickup it replaces | the livraison method replaces retrait, and this is announced; address form, checkDeliverable, getDeliveryPrice |
| "Finalise" + confirmation | placeOrder, then createDelivery with body.fee | order placed, receipt in the room |
| the phone given for delivery | identifies: true, written +221… | the room keeps it; no form asks it again |
| "send me a code" | send_verification_code → sendOtp | the restaurant sends a text; the model sees neither the number nor the code |
| the code typed | check_verification_code → checkOtp | number verified, the room belongs to the customer |
| the last dish removed (before the submit) | depends_on + on_loss: clear | delivery goes with it |
Key points
kinds.pickup, with itsselect: {}and itsmode: selfsubmit, is what prevents charging delivery to someone who comes to collect their meal.body.fee: "$item.price"passes on the computed price. Without it, Core refuses the station: the customer would have seen an amount the restaurant does not receive.remiseis aone_ofof two methods on one skill, each one of its kinds: only one does the task, and a selection that kept both could not be submitted.after: [plats]opens "Pickup or delivery" once the dishes are chosen, within the journey;depends_onon the skill holds the same dependency, even outside the journey.