spec / action grammar v0.1

The action grammar.

The shared vocabulary every assistant we work with has learned. Typed contracts, declared invariants, behaviour you can count on across vendors. Open spec, MIT-licensed.

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-brooklyn

Request 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_tgm2sx6rpa

Canonical 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 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 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 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 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 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 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 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

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/mcp

Returns 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:

json
{
  "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:

  1. Add the literal to ActionType in lib/action-grammar/types.ts.
  2. Add a row to PRODUCT_INDEX, PRODUCT_ACTION_TYPES, and SCHEMAS in lib/action-grammar/catalog.ts.
  3. Add a handler clause in the relevant product's submit* function in lib/action-grammar/submit.ts and (if write) a case in lib/action-grammar/status.ts.

New workflows do not need new SaaS products, new manifests, or new endpoints. The grammar is the unit of growth.