Whatcanido Action Grammar v0.1
A business-level action taxonomy that sits above per-vertical SaaS products. Working draft, 2026-05-17.
What this is
Whatcanido runs four SaaS products: LeadKit (lead intake and quotes), Bookio (appointment booking), ProjectKit (client and project ops), and CRM (contacts, deals, invoices). Every provider on every product gets an agent-callable manifest. That works, but it puts a cognitive tax on the calling agent: which product is this provider on, what shape do its actions take, what does it accept as input?
The action grammar layer collapses that tax. Agents talk to one URL, see one taxonomy, submit one normalized input shape. The grammar layer translates into the right product's native create call underneath.
Google indexed information.
Whatcanido indexes actions.
The four-call shape
find_providers(action_type, query?, location?) -> UnifiedProvider[]
get_provider_actions(provider_id) -> ActionType[] + schemas
submit_action(provider_id, action_type, inputs) -> { request_id, status, ... }
get_action_status(request_id) -> { status, status_label, ... }Agents that speak MCP point at https://whatcanido.dev/api/mcp and get
exactly these four tools. Non-MCP clients use the equivalent REST
surfaces under /api/actions/*.
Action types
The grammar covers ten business-level verbs. New verbs are added by extending the catalog, not by writing per-vertical code.
| Action type | Description | Implemented by |
|---|---|---|
submit_request |
Free-form service request | LeadKit, ProjectKit, CRM |
request_quote |
Priced quote on a specific scope | LeadKit, CRM |
book_slot |
Reserve a calendar appointment | Bookio |
ask_availability |
Read-only: list free slots | Bookio |
cancel_booking |
Cancel a confirmed booking | Bookio |
create_ticket |
Support / question / bug | ProjectKit |
start_project |
Initiate a new project from a brief | ProjectKit |
pay_invoice |
Surface a payable invoice and its public payment URL | CRM |
record_activity |
Log a note / call / email / meeting on a contact | CRM |
list_services |
Read-only: catalog of offerings | LeadKit, Bookio |
The product order in each row is meaningful: products earlier in the list
outrank later ones when discover ranks providers, so e.g. submit_request
favours LeadKit (the dedicated intake product) over ProjectKit and CRM.
list_services is intentionally scoped to LeadKit and Bookio because they
are the only products that maintain a fixed service catalog; CRM and
ProjectKit work with bespoke scope per engagement.
Identifiers
Provider id. . The slug is the same one the product
already used for that tenant. Examples:
leadkit:north-bureau
bookio:studio-sangha
projectkit:atelier-meridian
crm:acme-consulting-brooklynRequest id. . Round-trippable through
get_action_status without a separate database:
leadkit:lead:ld_kfo0mxrqb6
bookio:booking:bk_mf4aggf9
projectkit:project:prj_4fc1a0e8d3
crm:deal:dl_tgm2sx6rpaCanonical input schemas
Each action type has one canonical schema. Per-product handlers consume the subset they need and translate onto product-specific fields. Agents collect inputs once, regardless of which product the provider runs on.
submit_request
| Field | Type | Required | Notes |
|---|---|---|---|
contact_name |
string | yes | |
contact_email |
yes | ||
message |
text | yes | Free-form description of the work. |
contact_phone |
phone | no | |
service_id |
string | no | If a service has been picked from the catalog. |
budget |
currency | no | |
budget_currency |
string | no | ISO 4217. |
deadline |
date | no | YYYY-MM-DD. |
agent_vendor |
string | no | claude.ai, chatgpt.com, etc. |
request_quote
| Field | Type | Required | Notes |
|---|---|---|---|
contact_name |
string | yes | |
contact_email |
yes | ||
scope |
text | yes | What the provider should price. |
service_id |
string | no | |
contact_phone |
phone | no | |
budget_hint |
currency | no | |
currency |
string | no | ISO 4217. |
deadline |
date | no | |
agent_vendor |
string | no |
book_slot
| Field | Type | Required |
|---|---|---|
service_id |
string | yes |
date |
date | yes |
time |
time | yes |
customer_name |
string | yes |
customer_phone |
phone | yes |
staff_id |
string | no |
customer_email |
no | |
notes |
text | no |
agent_vendor |
string | no |
Call ask_availability first to confirm a slot is bookable. The submit
handler re-checks availability and rejects with slot_unavailable if the
slot was claimed between the two calls.
ask_availability
| Field | Type | Required |
|---|---|---|
service_id |
string | yes |
date |
date | yes |
staff_id |
string | no |
Read-only. Returns data.slots: string[] of HH:MM 24-hour times.
cancel_booking
| Field | Type | Required | Notes |
|---|---|---|---|
booking_id |
string | yes | Either the raw booking id (bk_abc123) or the full request_id (bookio:booking:bk_abc123) as returned by book_slot. |
reason |
text | no | Optional reason logged with the cancellation. |
agent_vendor |
string | no |
Idempotent: cancelling an already-cancelled booking returns ok with the
existing cancelled_at. If the booking is held by a different Bookio
tenant, the server returns booking_belongs_to_other_provider with the
correct matched_provider_id so the agent can retry.
create_ticket
| Field | Type | Required |
|---|---|---|
client_name |
string | yes |
client_email |
yes | |
title |
string | yes |
body |
text | yes |
kind |
enum | no |
priority |
enum | no |
project_id |
string | no |
agent_vendor |
string | no |
start_project
| Field | Type | Required |
|---|---|---|
client_name |
string | yes |
client_email |
yes | |
project_name |
string | yes |
brief |
text | yes |
budget |
currency | no |
currency |
string | no |
start_date |
date | no |
due_date |
date | no |
agent_vendor |
string | no |
Creates the client contact, the project, and seeds tasks plus milestones from the provider's business-type template.
pay_invoice
| Field | Type | Required | Notes |
|---|---|---|---|
invoice_number |
string | yes | The human-readable invoice number printed on the bill, e.g. INV-0411. Scoped to the provider's tenant. |
payer_email |
no | Email of the person settling the invoice. Logged on the activity feed. | |
payer_name |
string | no | |
agent_vendor |
string | no |
Looks up the invoice by number inside the provider's CRM tenant. Returns the invoice status, amount, currency, due date, and the public URL where the client can view and pay. Logs an activity on the related contact so the provider sees who initiated the payment flow. Does not move money on its own; the public invoice page handles the Stripe Checkout when the tenant has connected Stripe.
record_activity
| Field | Type | Required | Notes |
|---|---|---|---|
contact_email |
yes | Lookup key. The provider's CRM resolves which contact this attaches to by email. If no contact matches, call submit_request first. |
|
kind |
enum | yes | note / call / email_sent / email_received / meeting / task |
summary |
string | yes | Headline shown in the activity feed. |
body |
text | no | Longer body, transcript, notes. |
deal_id |
string | no | Attach the activity to a specific deal. |
agent_vendor |
string | no |
Append a touchpoint to an existing contact's activity log. Use after an
agent has had a real interaction with the client (call, email, meeting,
note) and the provider's CRM should reflect it. Returns
crm:activity: which round-trips through get_action_status.
list_services
No inputs. Returns the provider's service catalog. Some products (CRM, ProjectKit) do not maintain a fixed service catalog and return an empty list with an informational message.
Transport
MCP (recommended)
POST https://whatcanido.dev/api/mcp
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "find_providers",
"arguments": { "action_type": "submit_request", "query": "landing page" }
}
}The server speaks JSON-RPC 2.0 over HTTP. Batched requests are supported. Notifications get no response. Streaming is intentionally not implemented: every tool returns its full result synchronously.
Server info:
GET https://whatcanido.dev/api/mcpReturns the server name, version, protocol version, the four tool names, and pointers to the equivalent REST routes.
REST
For non-MCP clients (curl, browser fetches, server-side calls):
GET /api/actions/discover?action_type=<type>&query=<text>&city=<city>&country=<country>&industry=<industry>&product=<product>&limit=<n>
GET /api/actions/schema?provider_id=<id>&action_type=<type>
GET /api/actions/schema?action_type=<type>
POST /api/actions/submit
Body: { provider_id, action_type, inputs }
GET /api/actions/status?request_id=<id>All routes return application/json with permissive CORS so they can be
called from arbitrary origins.
Validation and error shape
Inputs are validated against the canonical schema before any vertical is touched. Missing required fields produce a structured error so the calling agent can ask the user without re-submitting:
{
"ok": false,
"error": "validation_failed",
"missing_fields": ["contact_email", "message"],
"detail": {
"invalid": [
{ "field": "deadline", "reason": "expected_YYYY-MM-DD" }
]
}
}Other canonical error codes:
| Code | Meaning |
|---|---|
provider_not_found |
provider_id does not resolve. |
action_type_not_supported_by_provider |
The provider does not implement this action. The detail.supported array lists what they do. |
service_not_found |
The provided service_id is not in the provider's catalog. The detail.available array lists valid ones. |
slot_unavailable |
The requested time is no longer free. The detail.available_slots array gives current options. |
invoice_not_found |
pay_invoice could not find an invoice with that number on the provider. detail.available lists open invoices. |
invoice_void |
The invoice exists but is void. |
contact_not_found |
record_activity could not find a contact with that email on any CRM tenant. |
contact_in_other_tenant |
record_activity found the contact on a different CRM tenant. detail.matched_provider_id and detail.matched_contact_id point to the right tenant; retry there. |
request_not_found |
Status lookup miss. |
invalid_request_id |
Request id is not in the shape. |
City and country normalization
find_providers normalises common city and country endonym/exonym pairs
so agents driving in the user's native language do not get zero results
just because the seed data is in English. city=Praha matches a provider
whose data says Prague. country=Česko matches Czech Republic.
Substring matching is also enabled, so New York matches New York City
and vice versa.
Audit
Every grammar-level submit writes an audit event into aamkit_audit with
tenant_slug set to the synthetic id so cross-product
agent traffic can be filtered across the whole grammar layer at once.
Underneath, the underlying vertical also writes its own audit row through
its native path, so per-product compliance audits remain intact.
Extending the grammar
Adding a new business-level verb (pay_invoice, place_order,
accept_proposal, ...) is a code change in three places:
- Add the literal to
ActionTypeinlib/action-grammar/types.ts. - Add a row to
PRODUCT_INDEX,PRODUCT_ACTION_TYPES, andSCHEMASinlib/action-grammar/catalog.ts. - Add a handler clause in the relevant product's
submit*function inlib/action-grammar/submit.tsand (if write) acaseinlib/action-grammar/status.ts.
New workflows do not need new SaaS products, new manifests, or new endpoints. The grammar is the unit of growth.