Verify and publish
What Core says at load time
Each defect has a severity, and the severity decides what the person sees.
| severity | effect | example |
|---|---|---|
| error | the station refuses to start | a submit call whose tool names no operation; two skills on the same kind |
| withhold | this skill is not exposed; the rest works | a skill whose invoke names a capability that does not exist |
| note | written for the author; nothing changes | a playbook that no skill opens |
A few refusals worth knowing:
| written | refused because |
|---|---|
an update: {price: …} guard without the submit passing $item.price | the customer would see a price the tenant never receives |
a condition (require, when) badly written, or reading an undeclared field | a require that reads nothing would refuse every item |
$item.id in an idempotency field | see The submit |
a $submitted[…] citing a call no playbook task runs, or an undeclared output | it would be empty, and the tenant would receive a blank with ok: true; see The submit |
two calls in one submit | a retry after the second failed would send the first again |
a required task whose skill produces no kind | the journey would announce "ready" on an empty selection |
a task that is not exactly one of skill, one_of, all_of; a one_of with fewer than two methods, or with no method without when; two methods that take the same kind | nobody could tell what the task does, nor which method a line is; see Playbooks |
a task's or a method's when that reads anything but $selection.kinds, looks for a kind the station does not have, or a kind of its own task (for an all_of, of the tasks it joins) | the condition could not be checked, would never hold, or would close the task for ever |
| a skill in two tasks of a playbook | a line would not say which task it belongs to |
an after or an all_of naming a missing task or a task with no kind, or that loops; an all_of over tasks ordered between them | the task would never open |
a required task after an optional one, directly or through an all_of | the optional task would be required in practice |
present, select, submit, countable, depends_on or on_loss on a skill without kinds | its rows are not items: nothing would read these keys |
several kinds with one lacking when; a kind's when that reads anything but a field of present.fields ($fields.<name>) | nothing would say which kind a row is; an undeclared row key is never read |
invoke with both method and native, or neither; method: search_products | method names an operation of the station's APIs; Convoy's capabilities are written native |
an undeclared $params.x, a parameter nothing reads, an invoke.args argument the capability does not take | a parameter nothing sets or nothing reads would mislead the reader; the API would ignore an unknown argument and answer as though it had not been sent |
a parameter without default read by invoke.args, or that no task binds; a task's params that do not bind $submitted[<task>.<call>].<output> of another task of the playbook, never skipped, whose every method makes the call | the call would leave with a blank value; see Playbooks |
an unknown render.component | a typo would silently draw something else |
component: report without render.report | nothing says which field is the answer: the person would read prose instead of the report |
render.report with a component other than report | nothing would read it |
two skills or two playbooks with the same name | the second would replace the first, without a word |
a submit_guard.validate that names no operation | no submit could ever go through |
catalog.mode: indexed without search_text_fields | every row would be indexed on empty text |
| a key Core does not know, at any depth | it would suggest it has an effect; the provisioner's API answers 422 with unknown_keys |
The tests
# the examples in this guide, compiled by Core
pytest convoy/packages/convoy-core/tests/test_docs_examples.py
# a running station: the probes replay defects that were actually shipped
./scripts/readiness/all.sh
Publish
- File: drop
<tenant_id>.yamlinto the deployment's configs folder. - API: the provisioner first runs the same check as the station. A config with an error
gets a 422 listing the faults (
where,message,remedy), and nothing is written. Otherwise it writes the config to the database and starts the tenant's containers. It answersreadyonly once every server serves this tenant: the MCP has its config and exposes the skills Core compiles, the agent has its history. Otherwise the tenant isdegraded, and the response (502) says why.
Between two changes, the provisioner looks at each ready station every minute. If a server stops,
or no longer serves the station, it goes degraded; once the server serves again, it goes back to
ready. Each move is recorded in the log (/runs).
Either way, the station recompiles at startup and shows every error and every note. Read the notes.
Update a station
POST /v1/tenants/{id}/redeploy, with no body, puts the station on the provisioner's Core and
images, with the config it already has. If that Core now refuses the config, the answer is a 422
with the faults, and no server is touched. For the whole fleet: make stations-build, then
make stations-redeploy.
A station only runs images built with the same Core as the provisioner. Otherwise the change gets a
409 core mismatch, and nothing is modified.
To go back to a config that worked: GET /v1/tenants/{id}/versions lists the last ten versions,
and POST /v1/tenants/{id}/rollback with {"version": n} applies that version's config again.
To know beforehand which stations would be refused: python -m convoy_provisioner.admin check-configs gives each station's verdict, and the provisioner also logs it at startup.
To know whether a station still runs its author's file: POST /v1/tenants/{id}/compare with the
config answers same and the list of keys that differ, and changes nothing.
Remove
DELETE /v1/tenants/{id} sets the tenant to deleting, then removes its servers, its config and
its route. Meanwhile any change gets a 409. If a step fails, the response (502) says what is
already removed: sending the same DELETE again finishes the job. The log (/runs) stays in the
database after the delete, so you can tell who removed what.
Two changes to the same tenant never run at once: the second gets a 409 busy straight away.
Send it again once the first one is done.