MCP tools


Every tool the Layout MCP server publishes, with the JSON Schema of its input and its output, read off the server's own registry. The same contract is machine-readable at mcp-tools.json.

Calling a tool

The server at https://mcp.layout.link speaks MCP over Streamable HTTP and keeps no session: every request is one JSON-RPC 2.0 POST that carries its own credential, so tools/list and tools/call work without a session id. Any MCP client works. To call it by hand, send Accept: application/json, text/event-stream.

curl https://mcp.layout.link \
  -H "Authorization: Bearer $LAYOUT_GRANT" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "order",
      "arguments": { "action": "preview", "restaurant": "Layout test kitchen", "query": "Layout test kitchen", "near": "Rocklin, CA" }
    }
  }'

The answer is text/event-stream carrying one message event, whose data is the JSON-RPC response:

event: message
data: {"result":{"content":[{"type":"text","text":"…"},{"type":"text","text":"{\"action\":\"preview\",…}"}],"structuredContent":{"action":"preview","preview":{…}}},"jsonrpc":"2.0","id":1}
  • content is text. When Layout has a sentence for the person, the first block is that sentence alone; the last block is the whole result as JSON.
  • structuredContent is the same result as an object, and matches the tool's output schema below.
  • A refusal is a tool result, not a JSON-RPC error. It has isError: true and comes in one of two forms. A refused action on a tool this session lists is one JSON text block whose error is forbidden (this session may never make that call), rate_limited, invalid_input, unauthorized or internal. A tool this session does not list is answered by the MCP SDK with plain text, MCP error -32602: Tool <name> not found, which is not JSON and has no error field. Call tools/list first and call only the tools it returns.

Before the call

A few answers come before any JSON-RPC runs. They are plain HTTP with the API’s error envelope, { "error": { "code", "message" } }, and carry no jsonrpc, result or event stream:

StatusBodyWhen, and what to do
401{"error":{"code":"unauthorized","message":"This build session is not valid."}}A build grant that is expired or revoked. A grant lasts 15 minutes unless you refresh it, so this is the common case: refresh it, or mint a new one, with POST /v1/users/{userId}/grant.
401{"error":{"code":"unauthorized","message":"Missing, invalid or disconnected access token. Authorize with Layout again."}}No bearer, one Layout does not recognise, or an OAuth access token that is expired or whose connection ended. Refresh the token, or run consent again. It carries WWW-Authenticate: Bearer resource_metadata="https://mcp.layout.link/.well-known/oauth-protected-resource", scope="order".
403{"error":{"code":"forbidden","message":"Connect through https://mcp.layout.link."}}The request did not come through Layout’s MCP host. Call https://mcp.layout.link.
503{"error":{"code":"unavailable","message":"…"}}Layout could not check the credential, or its edge is down. Nothing ran. Retry with backoff.

The order actions

order carries every step of an order in its action argument. Each one is a tools/call with these arguments:

{ "action": "preview", "restaurant": "Layout test kitchen", "query": "Layout test kitchen", "near": "Rocklin, CA" }

{ "action": "build", "restaurant": "Layout test kitchen", "placeId": "ChIJbloomCoffee",
  "items": ["large iced oat latte"], "idempotencyKey": "7f3c2a9e-build-1" }

{ "action": "status", "idempotencyKey": "7f3c2a9e-build-1" }

{ "action": "confirm", "orderId": "0b6f2a54-…", "expectedTotalMinor": 750,
  "idempotencyKey": "7f3c2a9e-confirm-1", "code": "123456" }

{ "action": "status", "orderId": "0b6f2a54-…" }

code goes on a confirm only when a code was asked for, and a production build grant’s confirm always asks on the person’s first order through your application. On a build grant, cancel: true must name the orderId of a cart your application built; without one the call is refused. A build holds for up to about 15 seconds and returns the cart if it lands in that time, else building: poll with status or order_status. order_status takes the same idempotencyKey or orderId and is the read-only way to follow either. Which actions a session may call is in the table under each tool, and Ordering over MCP walks the flow.

A tool outside the session

A production build grant calling menu, a tool its tools/list does not return. The SDK answers with plain text, not JSON, and no error field. Call only tools that tools/list returns. Recorded from the server:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "menu",
    "arguments": {
      "placeId": "ChIJbloomCoffee"
    }
  }
}
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "MCP error -32602: Tool menu not found"
      }
    ],
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 2
}

order

Place or preview a food order for the user. Can change state.

SessionCan call
OAuth connectionYes
Production build grantpreview, build, status, request_code, confirm, cancel
Sandbox build grantpreview, build, status, request_code, confirm, cancel

Input

ArgumentTypeRequiredDescription
action"preview" | "build" | "confirm" | "status"Yes'preview' = resolve + estimate. 'build' = START building a cart (returns 'building'; poll status). 'status' = poll a build by idempotencyKey, OR poll PLACEMENT by orderId after confirm returns 'placing'. 'confirm' = finalize with the code (or resend).
restaurantstringNoThe restaurant, cafe, shop or brand the user named to order FROM, in their exact words ('602 Coffee', 'Starbucks', 'Jack in the Box'). It is a BUSINESS, never a place on a map: a name that starts with a number ('602 Coffee', '7 Leaves') is still a business, do not read it as an address, do not fold it into `near`, and never replace it with the city it is in. Fill this on preview AND on build whenever the user said where to order from; omit it only when they named no restaurant at all.
querystringNopreview: the RESTAURANT or brand name the user named ('Starbucks', '602 Coffee'), the same name you put in `restaurant`. NOT the item, size or 'decaf', and NOT a city: 'an iced americano at 602 coffee in Huntington Beach' is restaurant='602 Coffee', query='602 Coffee', near='Huntington Beach'; the drink waits for build. Only when they named NO restaurant is query the food they want ('iced americano'), and Layout finds places that serve it. build: EVERYTHING they asked for, in their own words, WITH every size, cup, temperature, and prep detail kept, 'a grande iced brown sugar oatmilk shaken espresso in a venti cup with extra ice' is passed WHOLE, never shortened to 'iced brown sugar oatmilk shaken espresso'. If they named two drinks, both belong here ('a grande latte and a grande cold brew'). Never drop a word of what they asked for. (Unused for confirm.)
itemsarray of stringNobuild, optional but STRONGLY preferred when they asked for more than one thing: the same order split into ONE ENTRY PER ITEM, each with its own size and customizations, ["grande iced pumpkin cream shaken espresso in a venti cup, extra ice", "grande iced brown sugar oatmilk shaken espresso"]. This does NOT replace `query`, `query` must still name everything. The list is what lets Layout check that every item actually reached the cart and tell the user by name if one did not.
nearstringNoWhere the user is, a street address, city, or area. Pass whatever you know (even approximate) so Layout can find the right store; it will ask the user to pick if it's not precise enough. Prefer `geo` when you have exact coordinates.
geoobjectNopreview: the user's DEVICE coordinates (lat/lng), the highest-confidence locality. It does not skip the which-store question (only a store address the user states does that); it makes the question name the nearest store as a one-tap default. If you do not have them and your client CAN get them, request device location permission from the user first, then pass the result here. Ask once per conversation; if declined, use `near` instead.
platformstringNobuild only: the site/spec id to build the cart on (e.g. 'carlsjr').
orderUrlstringNobuild only: the orderable URL for an unmapped (cold) site. One of platform|orderUrl required for build.
restaurantNamestringNobuild only: the resolved restaurant name (from preview), shown in the recap.
restaurantAddressstringNobuild only: the resolved restaurant address (from preview), shown in the recap.
placeIdstringNothe store's placeId, from preview's `resolved.placeId`, or from the `nearby` option the user picked. Send it on BUILD and on any follow-up PREVIEW after a pick. Pass it whenever you have it: it identifies WHICH BRANCH, so Layout checks that store's opening hours instead of matching a name and an address.
blacklistHoststringNobuild only: the resolved site host (from preview's openUrl), for the eligibility check.
modifiersstring | array of stringNobuild only: the customizations to apply, size, cup, temperature, extras, substitutions ('grande', 'in a venti cup', 'extra ice', 'oat milk', 'no whip'), free text or a list. `query` already carries them in the person's words; fill this too when they customized, so the runner has the customizations as an explicit checklist and not only buried in the item text. Never a reason to shorten `query`.
knownStoreIdstringNobuild only: a resolved store id, if a location is already chosen.
timeZonestringNobuild only: the user's IANA timezone (e.g. 'America/Los_Angeles') if you know it. It decides whether a pickup slot falls on THEIR today or a different day; without it Layout falls back to the store's own zone.
acceptPickupTimestringNobuild only: pass ONLY after a previous build returned status 'pickup_next_day' AND the user said yes. Set it to `pickupDecision.stated` VERBATIM. Never invent it and never set it to skip the question.
choicesstringNobuild only: the user's answer to a 'needs_choices' question ('bowl, white rice'), in THEIR own words. Pass it on the fresh build that follows their answer. Never invent choices they did not make and never set it to skip the question.
userInitiatedRetrybooleanNobuild only: set TRUE ONLY when the HUMAN explicitly asked to try again after a failed/blocked attempt. NEVER set it yourself to bypass a retry_blocked, it exists so a real user re-request isn't mistaken for an automatic retry.
cancelbooleanNoSet TRUE when the USER asks to cancel their order or cart. Works with any action value; pass orderId when you have it (omitted = the active order). The response's `cancel.status` is the only truth: 'canceled' = stopped, nothing charged; 'already_placing' = too late, the placement is in flight (poll order_status); 'placed' = it already went to the restaurant; 'no_active_order' = nothing to cancel. NEVER tell the user an order was canceled unless status 'canceled' actually came back.
idempotencyKeystringNobuild + confirm (REQUIRED): FRESH RANDOM id per NEW attempt (8+ random chars, never only date/food-derived, that collides across sessions). Reuse only to poll/confirm the SAME attempt.
orderIdstringNoconfirm (REQUIRED): the order to confirm. status: pass it (WITHOUT idempotencyKey semantics) to poll PLACEMENT after a confirm returned 'placing', keep polling until placement.status is placed/failed/challenge.
expectedTotalMinorintegerNoconfirm only (REQUIRED): the total the user approved (from the build recap).
stage"confirm" | "resend"Noconfirm only (optional, defaults to 'confirm'): omit it to finalize; pass 'resend' to text a new code.
codestringNoconfirm + stage 'confirm': the 6-digit code the user read back (omit if their toggle is off).
declineCreditbooleanNoconfirm only (optional): set true ONLY if the user explicitly says to place WITHOUT their Layout credit. Credit auto-applies and is shown on the card; omit this to keep it.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "action": {
      "type": "string",
      "enum": [
        "preview",
        "build",
        "confirm",
        "status"
      ],
      "description": "'preview' = resolve + estimate. 'build' = START building a cart (returns 'building'; poll status). 'status' = poll a build by idempotencyKey, OR poll PLACEMENT by orderId after confirm returns 'placing'. 'confirm' = finalize with the code (or resend)."
    },
    "restaurant": {
      "type": "string",
      "maxLength": 200,
      "description": "The restaurant, cafe, shop or brand the user named to order FROM, in their exact words ('602 Coffee', 'Starbucks', 'Jack in the Box'). It is a BUSINESS, never a place on a map: a name that starts with a number ('602 Coffee', '7 Leaves') is still a business — do not read it as an address, do not fold it into `near`, and never replace it with the city it is in. Fill this on preview AND on build whenever the user said where to order from; omit it only when they named no restaurant at all."
    },
    "query": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "preview: the RESTAURANT or brand name the user named ('Starbucks', '602 Coffee') — the same name you put in `restaurant`. NOT the item, size or 'decaf', and NOT a city: 'an iced americano at 602 coffee in Huntington Beach' is restaurant='602 Coffee', query='602 Coffee', near='Huntington Beach'; the drink waits for build. Only when they named NO restaurant is query the food they want ('iced americano'), and Layout finds places that serve it. build: EVERYTHING they asked for, in their own words, WITH every size, cup, temperature, and prep detail kept — 'a grande iced brown sugar oatmilk shaken espresso in a venti cup with extra ice' is passed WHOLE, never shortened to 'iced brown sugar oatmilk shaken espresso'. If they named two drinks, both belong here ('a grande latte and a grande cold brew'). Never drop a word of what they asked for. (Unused for confirm.)"
    },
    "items": {
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "maxItems": 10,
      "description": "build, optional but STRONGLY preferred when they asked for more than one thing: the same order split into ONE ENTRY PER ITEM, each with its own size and customizations — [\"grande iced pumpkin cream shaken espresso in a venti cup, extra ice\", \"grande iced brown sugar oatmilk shaken espresso\"]. This does NOT replace `query` — `query` must still name everything. The list is what lets Layout check that every item actually reached the cart and tell the user by name if one did not."
    },
    "near": {
      "type": "string",
      "description": "Where the user is — a street address, city, or area. Pass whatever you know (even approximate) so Layout can find the right store; it will ask the user to pick if it's not precise enough. Prefer `geo` when you have exact coordinates."
    },
    "geo": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number"
        },
        "lng": {
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false,
      "description": "preview: the user's DEVICE coordinates (lat/lng) — the highest-confidence locality. It does not skip the which-store question (only a store address the user states does that); it makes the question name the nearest store as a one-tap default. If you do not have them and your client CAN get them, request device location permission from the user first, then pass the result here. Ask once per conversation; if declined, use `near` instead."
    },
    "platform": {
      "type": "string",
      "maxLength": 100,
      "description": "build only: the site/spec id to build the cart on (e.g. 'carlsjr')."
    },
    "orderUrl": {
      "type": "string",
      "maxLength": 2000,
      "description": "build only: the orderable URL for an unmapped (cold) site. One of platform|orderUrl required for build."
    },
    "restaurantName": {
      "type": "string",
      "maxLength": 500,
      "description": "build only: the resolved restaurant name (from preview), shown in the recap."
    },
    "restaurantAddress": {
      "type": "string",
      "maxLength": 500,
      "description": "build only: the resolved restaurant address (from preview), shown in the recap."
    },
    "placeId": {
      "type": "string",
      "maxLength": 255,
      "description": "the store's placeId — from preview's `resolved.placeId`, or from the `nearby` option the user picked. Send it on BUILD and on any follow-up PREVIEW after a pick. Pass it whenever you have it: it identifies WHICH BRANCH, so Layout checks that store's opening hours instead of matching a name and an address."
    },
    "blacklistHost": {
      "type": "string",
      "maxLength": 500,
      "description": "build only: the resolved site host (from preview's openUrl), for the eligibility check."
    },
    "modifiers": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      ],
      "description": "build only: the customizations to apply — size, cup, temperature, extras, substitutions ('grande', 'in a venti cup', 'extra ice', 'oat milk', 'no whip'), free text or a list. `query` already carries them in the person's words; fill this too when they customized, so the runner has the customizations as an explicit checklist and not only buried in the item text. Never a reason to shorten `query`."
    },
    "knownStoreId": {
      "type": "string",
      "description": "build only: a resolved store id, if a location is already chosen."
    },
    "timeZone": {
      "type": "string",
      "maxLength": 60,
      "description": "build only: the user's IANA timezone (e.g. 'America/Los_Angeles') if you know it. It decides whether a pickup slot falls on THEIR today or a different day; without it Layout falls back to the store's own zone."
    },
    "acceptPickupTime": {
      "type": "string",
      "maxLength": 120,
      "description": "build only: pass ONLY after a previous build returned status 'pickup_next_day' AND the user said yes. Set it to `pickupDecision.stated` VERBATIM. Never invent it and never set it to skip the question."
    },
    "choices": {
      "type": "string",
      "maxLength": 300,
      "description": "build only: the user's answer to a 'needs_choices' question ('bowl, white rice'), in THEIR own words. Pass it on the fresh build that follows their answer. Never invent choices they did not make and never set it to skip the question."
    },
    "userInitiatedRetry": {
      "type": "boolean",
      "description": "build only: set TRUE ONLY when the HUMAN explicitly asked to try again after a failed/blocked attempt. NEVER set it yourself to bypass a retry_blocked — it exists so a real user re-request isn't mistaken for an automatic retry."
    },
    "cancel": {
      "type": "boolean",
      "description": "Set TRUE when the USER asks to cancel their order or cart. Works with any action value; pass orderId when you have it (omitted = the active order). The response's `cancel.status` is the only truth: 'canceled' = stopped, nothing charged; 'already_placing' = too late, the placement is in flight (poll order_status); 'placed' = it already went to the restaurant; 'no_active_order' = nothing to cancel. NEVER tell the user an order was canceled unless status 'canceled' actually came back."
    },
    "idempotencyKey": {
      "type": "string",
      "maxLength": 200,
      "description": "build + confirm (REQUIRED): FRESH RANDOM id per NEW attempt (8+ random chars — never only date/food-derived, that collides across sessions). Reuse only to poll/confirm the SAME attempt."
    },
    "orderId": {
      "type": "string",
      "maxLength": 100,
      "description": "confirm (REQUIRED): the order to confirm. status: pass it (WITHOUT idempotencyKey semantics) to poll PLACEMENT after a confirm returned 'placing' — keep polling until placement.status is placed/failed/challenge."
    },
    "expectedTotalMinor": {
      "type": "integer",
      "exclusiveMinimum": 0,
      "description": "confirm only (REQUIRED): the total the user approved (from the build recap)."
    },
    "stage": {
      "type": "string",
      "enum": [
        "confirm",
        "resend"
      ],
      "description": "confirm only (optional, defaults to 'confirm'): omit it to finalize; pass 'resend' to text a new code."
    },
    "code": {
      "type": "string",
      "description": "confirm + stage 'confirm': the 6-digit code the user read back (omit if their toggle is off)."
    },
    "declineCredit": {
      "type": "boolean",
      "description": "confirm only (optional): set true ONLY if the user explicitly says to place WITHOUT their Layout credit. Credit auto-applies and is shown on the card; omit this to keep it."
    }
  },
  "required": [
    "action"
  ],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "action": {
      "type": "string",
      "description": "Echo of the action performed."
    },
    "preview": {
      "type": "object",
      "properties": {
        "sayToUser": {
          "type": "string",
          "description": "Layout's own words for the user — relay this framing."
        },
        "needsLocation": {
          "type": "boolean",
          "description": "TRUE means this is a QUESTION, not a result: nothing is being built. Ask which store using sayToUser, and the moment the user picks one, call `order` again with action:'build'."
        },
        "nextAction": {
          "type": "string",
          "description": "The tool to call once the user answers (e.g. 'order' with action:'build')."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. Says exactly what to call once they pick a store. Never read it aloud."
        },
        "allClosed": {
          "type": "boolean",
          "description": "Every branch Layout can see is CLOSED right now. The which-one question has no true answer — do not ask it. Offer `openAlternatives` instead."
        },
        "openAlternatives": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "placeId": {
                "type": "string"
              },
              "distanceKm": {
                "type": "number"
              },
              "openNow": {
                "type": "boolean"
              },
              "closingSoon": {
                "type": "boolean"
              }
            },
            "additionalProperties": true
          },
          "description": "REAL places Layout CHECKED are open right now, nearest first — offered when the restaurant they asked for is closed, or when its nearest open branch is a drive away. Offer these BY NAME and build at whichever the user picks. NEVER substitute a restaurant from your own memory: you cannot know whether it is open, still in business, or even nearby."
        },
        "closedNote": {
          "type": "string",
          "description": "Layout's words for a resolved store that is closed right now. Relay it and STOP — do not build there unless the user says to anyway."
        },
        "closingSoonNote": {
          "type": "string",
          "description": "The store is OPEN and shuts too soon for an order to be placed and made in time, so Layout will refuse the build. Relay it and STOP. Do NOT say they are closed: they are open, and the limit is Layout's. Do not take an item for this store; offer to find somewhere open longer."
        },
        "notOrderableNote": {
          "type": "string",
          "description": "Layout READ this restaurant's own website and there is no way to place an order on it. Relay it and STOP. Unlike a closure this is final — do not build there even if the user asks again, and do not ask what they would like from it. Offer to find somewhere nearby that does instead."
        },
        "nearestOpen": {
          "$ref": "#/properties/preview/properties/openAlternatives/items",
          "description": "The nearest branch of the SAME brand that Layout CHECKED is open right now, when the one they named is closed. Offer this BEFORE `openAlternatives` and say the distance: they asked for this restaurant, and another branch of it beats a different one."
        }
      },
      "additionalProperties": true
    },
    "build": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "'building' = still working, keep polling. 'carted' = a REAL priced cart is ready — show confirmationCard verbatim. 'reached_bag_only' = the cart could NOT be verified: nothing was ordered or charged; never present items/prices as confirmed. 'closed_next_day' = their site isn't offering pickup today; relay userMessage and let the user decide. 'pickup_next_day' = the earliest pickup is a DIFFERENT DAY than the user's today, so the build STOPPED: nothing was ordered, there is no cart and nothing to confirm — relay userMessage, and only if they say yes, build again passing pickupDecision.stated as acceptPickupTime. 'needs_choices' = the restaurant REQUIRES a choice on this item (plate or bowl, which rice) that nobody has made, so the build stopped before spending anything: relay userMessage, offer exactly the options in choiceDecision.groups, and when the user answers, build again with a NEW idempotencyKey passing their answer as `choices`. Any other value: relay userMessage — do not improvise an explanation."
        },
        "choiceDecision": {
          "type": "object",
          "properties": {
            "needed": {
              "type": "boolean",
              "description": "True when the restaurant requires a choice before this item can be ordered."
            },
            "item": {
              "type": "string",
              "description": "The RESTAURANT's own name for what they asked for. Use this name, not the user's words."
            },
            "itemNamedBySite": {
              "type": "boolean",
              "description": "True when `item` is the restaurant's own name for it, false when it is the user's words."
            },
            "question": {
              "type": "string",
              "description": "Layout's question, built from their menu. Relay it rather than composing your own."
            },
            "groups": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What the restaurant calls this choice."
                  },
                  "required": {
                    "type": "boolean"
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Their own option names. Offer ONLY these; never options you remember."
                  }
                },
                "additionalProperties": true
              }
            },
            "answerWith": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "description": "The action to call with their answer. Always 'build'."
                },
                "field": {
                  "type": "string"
                }
              },
              "additionalProperties": true,
              "description": "How an answer is expressed: a NEW build passing the user's reply as `choices`."
            }
          },
          "additionalProperties": true,
          "description": "Present on 'needs_choices'. The choices this restaurant requires before the item can be ordered."
        },
        "pickupDecision": {
          "type": "object",
          "properties": {
            "needed": {
              "type": "boolean",
              "description": "True when the user has to agree to this pickup time before anything is ordered."
            },
            "stated": {
              "type": [
                "string",
                "null"
              ],
              "description": "The earliest pickup, in the STORE's own words. Quote it verbatim; never convert the time."
            },
            "day": {
              "type": "string",
              "description": "'different' when the earliest pickup is not the user's today."
            },
            "basis": {
              "type": "string",
              "description": "How the pickup time was established. Diagnostic; not something to read out."
            },
            "timeZone": {
              "type": "string",
              "description": "The STORE's zone. Present so nobody converts `stated` into the user's."
            },
            "question": {
              "type": "string",
              "description": "Layout's question for the user. Relay it rather than composing your own."
            },
            "options": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              },
              "description": "The choices Layout's own clients draw as buttons."
            },
            "acceptWith": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string"
                },
                "field": {
                  "type": "string"
                },
                "value": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "additionalProperties": true,
              "description": "How a 'yes' is expressed: a NEW build passing this value as acceptPickupTime. There is no order to confirm."
            },
            "openAlternatives": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              },
              "description": "Places VERIFIED open right now. Offer only these; never a restaurant from your own memory."
            }
          },
          "additionalProperties": true,
          "description": "Present on 'pickup_next_day'. The pickup time the user has to agree to before anything is ordered."
        },
        "userMessage": {
          "type": "string",
          "description": "Layout's message for the user. Relay it rather than paraphrasing."
        },
        "confirmationCard": {
          "type": "string",
          "description": "Layout's branded confirmation. Show VERBATIM — never rewrite, reorder, or summarize."
        },
        "complete": {
          "type": "boolean",
          "description": "FALSE means this is not the final answer and no cart exists yet. Do not present a false result as an outcome: follow `nextAction`/`assistantInstruction` instead of stopping here."
        },
        "nextAction": {
          "type": "string",
          "description": "The tool to call next (e.g. 'order_status'), with the SAME idempotencyKey."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. When present, it says exactly what to do next and takes precedence over anything you would infer from `status`. Never read it aloud to the user."
        },
        "trackUrl": {
          "type": "string",
          "description": "Layout's live tracking page for THIS order. Give it to the user while they wait — it works even if this conversation ends, and it is the only thing they can watch on their own."
        },
        "actionUrl": {
          "type": "string",
          "description": "A link the USER must open to unblock the order (e.g. update a payment method). When present, give it to them — the order cannot continue until they do."
        }
      },
      "additionalProperties": true
    },
    "placement": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "'placed' is the ONLY status that means the order exists — never tell the user an order was placed on any other value. 'placing' = in progress, keep polling. 'failed' = NOT placed, nothing charged (relay userMessage). 'challenge' = the user was texted a secure approval link. 'unconfirmed' = Layout could not verify either way — tell the user to check before re-ordering; claim neither success nor failure."
        },
        "userMessage": {
          "type": "string"
        },
        "orderNumber": {
          "type": [
            "string",
            "number"
          ]
        },
        "complete": {
          "type": "boolean",
          "description": "FALSE means the placement is still running and this is NOT the outcome. Follow `nextAction`/`assistantInstruction`; never describe a false result as placed or failed."
        },
        "nextAction": {
          "type": "string",
          "description": "The tool to call next (e.g. 'order_status'), with the SAME orderId."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. Takes precedence over anything you would infer from `status`."
        },
        "trackUrl": {
          "type": "string",
          "description": "Layout's live tracking page for this order — give it to the user."
        }
      },
      "additionalProperties": true
    },
    "publicOrderId": {
      "type": "string",
      "description": "The developer-facing `ord_` id of this order, for the app's own `GET /v1/orders/:id` and webhooks. Present only when an application's own client drove the order. Never read it aloud."
    },
    "refusal": {
      "type": "object",
      "properties": {
        "code": {
          "type": "string",
          "enum": [
            "build_limit",
            "unavailable",
            "forbidden"
          ],
          "description": "The same code the REST API answers with."
        },
        "retryable": {
          "type": "boolean",
          "description": "True when the same call may succeed shortly. False on a spent daily ceiling: do not retry before limit.resetsAt."
        },
        "limit": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string",
              "const": "builds"
            },
            "scope": {
              "type": "string",
              "enum": [
                "person",
                "application",
                "account",
                "platform"
              ]
            },
            "max": {
              "type": "integer"
            },
            "resetsAt": {
              "type": "string"
            }
          },
          "required": [
            "name",
            "scope",
            "max",
            "resetsAt"
          ],
          "additionalProperties": false,
          "description": "The daily ceiling that refused the build."
        }
      },
      "required": [
        "code",
        "retryable"
      ],
      "additionalProperties": false,
      "description": "Present when Layout refused to start a build: nothing was built or charged. For code that does not read userMessage. Never read it aloud."
    },
    "cancel": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "'canceled' = the stop is real and complete; relay userMessage and do NOT confirm or poll this order again. 'already_placing' = TOO LATE to cancel — the placement is in flight; poll `order_status` and report only what it returns. 'placed' = the order already went to the restaurant; never describe it as canceled. 'no_active_order' = there was nothing to cancel."
        },
        "userMessage": {
          "type": "string",
          "description": "Layout's message for the user. Relay it rather than paraphrasing."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. Takes precedence over inference."
        }
      },
      "additionalProperties": true
    },
    "confirm": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "'placing' = the order is being placed RIGHT NOW; it is NOT placed — poll `order_status` with the orderId. 'code_sent' = a code was texted; ask the user to read it back. 'confirmed' = the cart is locked in. 'confirm_expired' / 'place_error' = relay userMessage; nothing was placed."
        },
        "userMessage": {
          "type": "string",
          "description": "Layout's message for the user. Relay it rather than paraphrasing."
        },
        "orderId": {
          "type": "string",
          "description": "Pass this to `order_status` to follow the placement."
        },
        "complete": {
          "type": "boolean",
          "description": "FALSE means the placement is still running — keep polling; do not report an outcome."
        },
        "nextAction": {
          "type": "string",
          "description": "The tool to call next (e.g. 'order_status')."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. Takes precedence over inference."
        },
        "trackUrl": {
          "type": "string",
          "description": "Layout's live tracking page for this order — give it to the user."
        }
      },
      "additionalProperties": true
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

order_status

Check on an order that is already running. Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantYes
Sandbox build grantYes

Input

ArgumentTypeRequiredDescription
idempotencyKeystringNoThe key used for the 'build' call, to follow that cart.
orderIdstringNoThe order id from a 'confirm' that returned 'placing', to follow the placement.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "idempotencyKey": {
      "type": "string",
      "maxLength": 200,
      "description": "The key used for the 'build' call, to follow that cart."
    },
    "orderId": {
      "type": "string",
      "maxLength": 100,
      "description": "The order id from a 'confirm' that returned 'placing', to follow the placement."
    }
  },
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "action": {
      "type": "string",
      "description": "Echo of the action performed."
    },
    "build": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "'building' = still working, keep polling. 'carted' = a REAL priced cart is ready — show confirmationCard verbatim. 'reached_bag_only' = the cart could NOT be verified: nothing was ordered or charged; never present items/prices as confirmed. 'closed_next_day' = their site isn't offering pickup today; relay userMessage and let the user decide. 'pickup_next_day' = the earliest pickup is a DIFFERENT DAY than the user's today, so the build STOPPED: nothing was ordered, there is no cart and nothing to confirm — relay userMessage, and only if they say yes, build again passing pickupDecision.stated as acceptPickupTime. 'needs_choices' = the restaurant REQUIRES a choice on this item (plate or bowl, which rice) that nobody has made, so the build stopped before spending anything: relay userMessage, offer exactly the options in choiceDecision.groups, and when the user answers, build again with a NEW idempotencyKey passing their answer as `choices`. Any other value: relay userMessage — do not improvise an explanation."
        },
        "choiceDecision": {
          "type": "object",
          "properties": {
            "needed": {
              "type": "boolean",
              "description": "True when the restaurant requires a choice before this item can be ordered."
            },
            "item": {
              "type": "string",
              "description": "The RESTAURANT's own name for what they asked for. Use this name, not the user's words."
            },
            "itemNamedBySite": {
              "type": "boolean",
              "description": "True when `item` is the restaurant's own name for it, false when it is the user's words."
            },
            "question": {
              "type": "string",
              "description": "Layout's question, built from their menu. Relay it rather than composing your own."
            },
            "groups": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "What the restaurant calls this choice."
                  },
                  "required": {
                    "type": "boolean"
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Their own option names. Offer ONLY these; never options you remember."
                  }
                },
                "additionalProperties": true
              }
            },
            "answerWith": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string",
                  "description": "The action to call with their answer. Always 'build'."
                },
                "field": {
                  "type": "string"
                }
              },
              "additionalProperties": true,
              "description": "How an answer is expressed: a NEW build passing the user's reply as `choices`."
            }
          },
          "additionalProperties": true,
          "description": "Present on 'needs_choices'. The choices this restaurant requires before the item can be ordered."
        },
        "pickupDecision": {
          "type": "object",
          "properties": {
            "needed": {
              "type": "boolean",
              "description": "True when the user has to agree to this pickup time before anything is ordered."
            },
            "stated": {
              "type": [
                "string",
                "null"
              ],
              "description": "The earliest pickup, in the STORE's own words. Quote it verbatim; never convert the time."
            },
            "day": {
              "type": "string",
              "description": "'different' when the earliest pickup is not the user's today."
            },
            "basis": {
              "type": "string",
              "description": "How the pickup time was established. Diagnostic; not something to read out."
            },
            "timeZone": {
              "type": "string",
              "description": "The STORE's zone. Present so nobody converts `stated` into the user's."
            },
            "question": {
              "type": "string",
              "description": "Layout's question for the user. Relay it rather than composing your own."
            },
            "options": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              },
              "description": "The choices Layout's own clients draw as buttons."
            },
            "acceptWith": {
              "type": "object",
              "properties": {
                "action": {
                  "type": "string"
                },
                "field": {
                  "type": "string"
                },
                "value": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "additionalProperties": true,
              "description": "How a 'yes' is expressed: a NEW build passing this value as acceptPickupTime. There is no order to confirm."
            },
            "openAlternatives": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  }
                },
                "additionalProperties": true
              },
              "description": "Places VERIFIED open right now. Offer only these; never a restaurant from your own memory."
            }
          },
          "additionalProperties": true,
          "description": "Present on 'pickup_next_day'. The pickup time the user has to agree to before anything is ordered."
        },
        "userMessage": {
          "type": "string",
          "description": "Layout's message for the user. Relay it rather than paraphrasing."
        },
        "confirmationCard": {
          "type": "string",
          "description": "Layout's branded confirmation. Show VERBATIM — never rewrite, reorder, or summarize."
        },
        "complete": {
          "type": "boolean",
          "description": "FALSE means this is not the final answer and no cart exists yet. Do not present a false result as an outcome: follow `nextAction`/`assistantInstruction` instead of stopping here."
        },
        "nextAction": {
          "type": "string",
          "description": "The tool to call next (e.g. 'order_status'), with the SAME idempotencyKey."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. When present, it says exactly what to do next and takes precedence over anything you would infer from `status`. Never read it aloud to the user."
        },
        "trackUrl": {
          "type": "string",
          "description": "Layout's live tracking page for THIS order. Give it to the user while they wait — it works even if this conversation ends, and it is the only thing they can watch on their own."
        },
        "actionUrl": {
          "type": "string",
          "description": "A link the USER must open to unblock the order (e.g. update a payment method). When present, give it to them — the order cannot continue until they do."
        }
      },
      "additionalProperties": true
    },
    "placement": {
      "type": "object",
      "properties": {
        "status": {
          "type": "string",
          "description": "'placed' is the ONLY status that means the order exists — never tell the user an order was placed on any other value. 'placing' = in progress, keep polling. 'failed' = NOT placed, nothing charged (relay userMessage). 'challenge' = the user was texted a secure approval link. 'unconfirmed' = Layout could not verify either way — tell the user to check before re-ordering; claim neither success nor failure."
        },
        "userMessage": {
          "type": "string"
        },
        "orderNumber": {
          "type": [
            "string",
            "number"
          ]
        },
        "complete": {
          "type": "boolean",
          "description": "FALSE means the placement is still running and this is NOT the outcome. Follow `nextAction`/`assistantInstruction`; never describe a false result as placed or failed."
        },
        "nextAction": {
          "type": "string",
          "description": "The tool to call next (e.g. 'order_status'), with the SAME orderId."
        },
        "assistantInstruction": {
          "type": "string",
          "description": "Written for YOU, not the user. Takes precedence over anything you would infer from `status`."
        },
        "trackUrl": {
          "type": "string",
          "description": "Layout's live tracking page for this order — give it to the user."
        }
      },
      "additionalProperties": true
    },
    "publicOrderId": {
      "type": "string",
      "description": "The developer-facing `ord_` id of this order, for the app's own `GET /v1/orders/:id` and webhooks. Present only when an application's own client drove the order. Never read it aloud."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

get_location

Get the user's location when you don't already have it, use this the moment they ask for something NEARBY ('near me', 'around here', 'closest one') and you have no coordinates and they named no place. Can change state. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

No arguments.

The input JSON Schema:

{
  "type": "object",
  "properties": {},
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "located": {
      "type": "boolean"
    },
    "geo": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number"
        },
        "lng": {
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false
    },
    "shareUrl": {
      "type": "string"
    },
    "userMessage": {
      "type": "string"
    },
    "assistantInstruction": {
      "type": "string"
    }
  },
  "required": [
    "located"
  ],
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

profile

Read or update the user's Layout food profile, the preferences Layout applies to every order: dietary restrictions ('vegetarian', 'no shellfish'), favorite restaurants, usual orders, the first name orders go under, and whether dietary restrictions are applied automatically. Can change state.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

ArgumentTypeRequiredDescription
action"get" | "update"Yes'get' = read the profile. 'update' = change it (needs at least one field below).
firstNamestringNoupdate: the first name orders are placed under.
applyDietaryRestrictionsbooleanNoupdate: whether Layout automatically applies the user's dietary notes when ordering.
addPreferencesarray of objectNoupdate: preference notes to save (max 5 per call).
removePreferenceIdsarray of stringNoupdate: ids (from 'get') of notes to remove.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "action": {
      "type": "string",
      "enum": [
        "get",
        "update"
      ],
      "description": "'get' = read the profile. 'update' = change it (needs at least one field below)."
    },
    "firstName": {
      "type": "string",
      "maxLength": 60,
      "description": "update: the first name orders are placed under."
    },
    "applyDietaryRestrictions": {
      "type": "boolean",
      "description": "update: whether Layout automatically applies the user's dietary notes when ordering."
    },
    "addPreferences": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "'dietary' | 'favorite_restaurants' | 'usual_orders' (anything else is filed as other)."
          },
          "note": {
            "type": "string",
            "maxLength": 200,
            "description": "The preference in the user's own words, e.g. 'no shellfish'."
          }
        },
        "required": [
          "category",
          "note"
        ],
        "additionalProperties": false
      },
      "maxItems": 5,
      "description": "update: preference notes to save (max 5 per call)."
    },
    "removePreferenceIds": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 10,
      "description": "update: ids (from 'get') of notes to remove."
    }
  },
  "required": [
    "action"
  ],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "action": {
      "type": "string"
    },
    "profile": {
      "type": "object",
      "properties": {
        "firstName": {
          "type": [
            "string",
            "null"
          ]
        },
        "applyDietaryRestrictions": {
          "type": "boolean"
        },
        "preferences": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "category": {
                "type": "string"
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "category",
              "note"
            ],
            "additionalProperties": true
          },
          "description": "The saved notes. Show notes, not ids; ids are only for removal."
        }
      },
      "additionalProperties": true
    },
    "sayToUser": {
      "type": "string",
      "description": "Layout's own wording for the user — relay it."
    },
    "manageUrl": {
      "type": "string",
      "description": "The user's full account page (contact info, addresses, cards live there — share this link when asked)."
    },
    "techWeek": {
      "type": "object",
      "properties": {
        "state": {
          "type": "string"
        },
        "amount": {
          "type": "string"
        },
        "window": {
          "type": "string"
        },
        "terms": {
          "type": "string"
        },
        "signupUrl": {
          "type": "string"
        },
        "termsUrl": {
          "type": "string"
        },
        "sayToUser": {
          "type": "string"
        },
        "assistantInstruction": {
          "type": "string"
        }
      },
      "required": [
        "state",
        "amount",
        "window",
        "terms",
        "signupUrl",
        "termsUrl",
        "sayToUser",
        "assistantInstruction"
      ],
      "additionalProperties": true,
      "description": "This account's SF Tech Week coffee and the published terms. Present only while the promotion runs."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

discover

Recommend WHERE the user should eat, drawn from what they have actually ordered through Layout. Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

ArgumentTypeRequiredDescription
walkablebooleanNoThey want somewhere they can WALK to ('walking distance', 'on foot'). Nothing past a short walk is suggested.
querystringNoThe kind of food they asked for ('healthy', 'tacos'). Used only when nothing they would like is walkable and Layout searches what is.
geoobjectNoThe user's device coordinates, when your client can read them. Without them Layout uses the last place it knows they ordered from, which is usually right, so this is worth passing but never required.
nearstringNoA city, town or ZIP the user NAMED ('Roseville', 'downtown Oakland', '95661'). Pass it whenever they say where they are or where they're asking about; it is resolved for free and beats the last place Layout saw them order.
openNowbooleanNoOnly places open right now. Use it when the ask is about eating NOW ('lunch', 'right now').
includeVisitedbooleanNoInclude places they already order from. Off by default, because a suggestion should be somewhere new, turn it on when they ask for a usual or a favourite.
limitintegerNoHow many to return. Default 5; lead with one regardless.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "walkable": {
      "type": "boolean",
      "description": "They want somewhere they can WALK to ('walking distance', 'on foot'). Nothing past a short walk is suggested."
    },
    "query": {
      "type": "string",
      "description": "The kind of food they asked for ('healthy', 'tacos'). Used only when nothing they would like is walkable and Layout searches what is."
    },
    "geo": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number"
        },
        "lng": {
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false,
      "description": "The user's device coordinates, when your client can read them. Without them Layout uses the last place it knows they ordered from, which is usually right — so this is worth passing but never required."
    },
    "near": {
      "type": "string",
      "description": "A city, town or ZIP the user NAMED ('Roseville', 'downtown Oakland', '95661'). Pass it whenever they say where they are or where they're asking about; it is resolved for free and beats the last place Layout saw them order."
    },
    "openNow": {
      "type": "boolean",
      "description": "Only places open right now. Use it when the ask is about eating NOW ('lunch', 'right now')."
    },
    "includeVisited": {
      "type": "boolean",
      "description": "Include places they already order from. Off by default, because a suggestion should be somewhere new — turn it on when they ask for a usual or a favourite."
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 12,
      "description": "How many to return. Default 5; lead with one regardless."
    }
  },
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "suggestions": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "place": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "cuisine": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "rating": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "openStatus": {
                "type": "string",
                "enum": [
                  "open",
                  "closed",
                  "unknown"
                ],
                "description": "READ THIS ONE. 'open' / 'closed' at the STORE's own clock right now; 'unknown' means Layout holds no hours for it, which is NOT closed — say you don't know."
              },
              "closesAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Store-local closing time when open, e.g. '5:00 PM'. Say it: 'open until 5' is the useful answer."
              },
              "opensAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Store-local next opening when closed, e.g. '7:00 AM' or 'Monday 7:00 AM'."
              },
              "openNow": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "The same answer as a boolean, for older clients. Prefer `openStatus`: a null here means unknown and is easy to misread as closed."
              },
              "closingSoon": {
                "type": "boolean",
                "description": "Open, and shutting within the hour. Say so: Layout stops taking orders shortly before a store closes, so this is the last window to place one."
              },
              "distanceKm": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "name"
            ],
            "additionalProperties": true
          },
          "reason": {
            "type": "string",
            "description": "Layout's computed reason. Relay it; do not rewrite it."
          },
          "becauseOf": {
            "type": "object",
            "properties": {
              "places": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Restaurants they have actually ordered from that led here."
              },
              "items": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Items they have actually ordered that led here."
              }
            },
            "required": [
              "places",
              "items"
            ],
            "additionalProperties": true
          },
          "visited": {
            "type": "boolean"
          }
        },
        "required": [
          "place",
          "reason",
          "becauseOf"
        ],
        "additionalProperties": true
      }
    },
    "nothingBecause": {
      "type": [
        "string",
        "null"
      ],
      "description": "'no_orders' | 'no_matches' — why the list is empty. They need different sentences."
    },
    "sayToUser": {
      "type": "string",
      "description": "Layout's own wording — relay it."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

places

Find restaurants, cafes and coffee shops BY DESCRIPTION OR NAME, 'is there a Starbucks in Oakland', 'coffee near me', 'somewhere open for breakfast', 'taco places downtown'. Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantYes
Sandbox build grantYes

Input

ArgumentTypeRequiredDescription
querystringYesWhat to look for, a name ('Starbucks'), a food ('tacos'), or a kind of place ('coffee shop').
nearstringNoThe RESOLVED locality, a real city and state, e.g. 'Oakland, CA' or 'San Francisco, CA'. YOU resolve it: if the user names a landmark, a neighbourhood, an airport or an abbreviation ('Salesforce Tower', 'the Mission', 'SFO', 'sf'), work out which city that is and pass THAT here. Layout matches this against the city each restaurant is in, so a landmark passed here matches nothing. Pass it whenever they said where they're asking about, a named place is where they are asking ABOUT, which is not always where they are.
nearRawstringNoThe user's OWN words for the place, verbatim, 'Salesforce Tower', 'the Mission', 'near the ballpark'. Pass it alongside `near` whenever they said something more specific than a city. If Layout has to go and look the area up, this is what it asks with, and it resolves landmarks far better than a city name alone. Omit when they only ever named a city.
geoobjectNoThe user's device coordinates, when your client can read them. Used to rank by real distance and to bound 'near me'.
openNowbooleanNoOnly places open right now, judged at the STORE's own clock. Use it when the ask is about eating now.
limitintegerNoHow many to return. Default 5.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "What to look for — a name ('Starbucks'), a food ('tacos'), or a kind of place ('coffee shop')."
    },
    "near": {
      "type": "string",
      "maxLength": 200,
      "description": "The RESOLVED locality — a real city and state, e.g. 'Oakland, CA' or 'San Francisco, CA'. YOU resolve it: if the user names a landmark, a neighbourhood, an airport or an abbreviation ('Salesforce Tower', 'the Mission', 'SFO', 'sf'), work out which city that is and pass THAT here. Layout matches this against the city each restaurant is in, so a landmark passed here matches nothing. Pass it whenever they said where they're asking about — a named place is where they are asking ABOUT, which is not always where they are."
    },
    "nearRaw": {
      "type": "string",
      "maxLength": 200,
      "description": "The user's OWN words for the place, verbatim — 'Salesforce Tower', 'the Mission', 'near the ballpark'. Pass it alongside `near` whenever they said something more specific than a city. If Layout has to go and look the area up, this is what it asks with, and it resolves landmarks far better than a city name alone. Omit when they only ever named a city."
    },
    "geo": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number"
        },
        "lng": {
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false,
      "description": "The user's device coordinates, when your client can read them. Used to rank by real distance and to bound 'near me'."
    },
    "openNow": {
      "type": "boolean",
      "description": "Only places open right now, judged at the STORE's own clock. Use it when the ask is about eating now."
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10,
      "description": "How many to return. Default 5."
    }
  },
  "required": [
    "query"
  ],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "places": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "placeId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "cuisine": {
            "type": [
              "string",
              "null"
            ]
          },
          "rating": {
            "type": [
              "number",
              "null"
            ]
          },
          "priceLevel": {
            "type": [
              "number",
              "null"
            ]
          },
          "openStatus": {
            "type": "string",
            "enum": [
              "open",
              "closed",
              "unknown"
            ],
            "description": "READ THIS ONE. 'open' / 'closed' at the STORE's own clock right now; 'unknown' means Layout holds no hours for it, which is NOT closed — say you don't know its hours."
          },
          "closesAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store-local closing time when open, e.g. '5:00 PM'. Say it: 'open until 5' is the useful answer."
          },
          "opensAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Store-local next opening when closed, e.g. '7:00 AM' or 'Monday 7:00 AM'."
          },
          "openNow": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "The same answer as a boolean, for older clients. Prefer `openStatus`: a null here means unknown and is easy to misread as closed."
          },
          "closingSoon": {
            "type": "boolean",
            "description": "Open, and shutting within the hour. Say so: Layout stops taking orders shortly before a store closes, so this is the last window to place one."
          },
          "distanceKm": {
            "type": [
              "number",
              "null"
            ],
            "description": "Straight-line km from the coordinates you passed. Absent when you passed none."
          }
        },
        "required": [
          "placeId",
          "name"
        ],
        "additionalProperties": true
      }
    },
    "notOpenNow": {
      "type": "array",
      "items": {
        "$ref": "#/properties/places/items"
      },
      "description": "Real restaurants Layout holds in that area that are NOT open right now, soonest to open first. Present ONLY when the open-now filter is what emptied `places`: it means Layout searched and FOUND places, so an empty `places` beside it is never 'there is nothing there'. These are NOT offerable — read each row's `openStatus` ('closed' with `opensAt` for when it reopens, 'unknown' when Layout holds no hours) and never offer to order from one now."
    },
    "stillChecking": {
      "type": "boolean",
      "description": "Layout found places in that area and has not finished working out whether they take online orders — that runs after this answer and usually lands within a minute. The list may grow shortly, though it is not certain it will. When this is true, an EMPTY list is NOT a finding that there is nothing there: never say there is nothing, say you are still checking and offer to look again in a moment."
    },
    "sayToUser": {
      "type": "string",
      "description": "Layout's own wording — relay it."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

place_facts

Answer an EVALUATIVE question about ONE restaurant you already have a `placeId` for, 'is it vegetarian', 'can I sit down there', 'do they do takeout', 'is it good for a group', 'do they serve breakfast'. Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

ArgumentTypeRequiredDescription
placeIdstringYesThe Google place id from a places, discover, or order result. Required, this tool never resolves a name.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "placeId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 400,
      "description": "The Google place id from a places, discover, or order result. Required — this tool never resolves a name."
    }
  },
  "required": [
    "placeId"
  ],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "attributes": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "source": {
              "type": "string"
            },
            "asOf": {
              "type": "string"
            },
            "dietary": {
              "type": "object",
              "properties": {},
              "additionalProperties": true
            },
            "service": {
              "type": "object",
              "properties": {},
              "additionalProperties": true
            },
            "goodFor": {
              "type": "object",
              "properties": {},
              "additionalProperties": true
            }
          },
          "additionalProperties": true
        },
        {
          "type": "null"
        }
      ]
    },
    "assistantInstruction": {
      "type": "string",
      "description": "Written for YOU, not the user. It says what these fields do and do not let you claim. Never read it aloud."
    }
  },
  "required": [
    "attributes"
  ],
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

help

Answer a question ABOUT LAYOUT ITSELF from Layout's own published help centre, refunds, when and how the user is charged, spending limits, changing or cancelling an order, what happens when an order is wrong or late, card safety, what data Layout stores, profile and dietary preferences, where Layout can order from, and how to set Layout up in an assistant or on a phone. Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

ArgumentTypeRequiredDescription
questionstringYesWhat the user wants to know, in their own words or as keywords, 'how do refunds work', 'when am I charged', 'can I cancel an order'.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "question": {
      "type": "string",
      "minLength": 1,
      "maxLength": 300,
      "description": "What the user wants to know, in their own words or as keywords — 'how do refunds work', 'when am I charged', 'can I cancel an order'."
    }
  },
  "required": [
    "question"
  ],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "articles": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The public help centre page. GIVE THIS TO THE USER with your answer."
          },
          "excerpt": {
            "type": "string",
            "description": "The part of the article about their question. Answer from this and nothing else."
          }
        },
        "required": [
          "title",
          "url",
          "excerpt"
        ],
        "additionalProperties": true
      }
    },
    "assistantInstruction": {
      "type": "string",
      "description": "Written for YOU, not the user. It says what you may and may not assert. Never read it aloud."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

menu

Look up something SPECIFIC on a restaurant's LIVE menu, read from their own ordering site, so items and prices are real, not from memory. Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

ArgumentTypeRequiredDescription
querystringNoRestaurant name to look up (with `near` or `geo` for locality).
placeIdstringNoA specific location's place id, when one is already settled, from an earlier order preview or a location the user already picked. Skips the which-location question.
nearstringNoThe user's area, to pick the right location.
geoobjectNoDevice coordinates, the strongest locality signal. If you do not have them and your client can request device location permission, do that first and pass the result here; otherwise use `near`.
orderUrlstringNoA specific location's ordering URL from an earlier preview, skips name resolution.
categorystringNoA section name from a previous 'categories' answer, EXACTLY as given, reads just that section.
intentstringNoThe user's FULL request in their own words (e.g. 'what's under $2 at McDonald's'). Always pass it, it's used only if the live read fails, so the web-knowledge fallback answers exactly what they asked.
mode"live" | "knowledge"NoDefaults to 'live' (reads their real site: ~a minute, a real browsing session). Pass 'knowledge' for RECOMMENDATIONS, 'what should I get at X', 'what are they known for', where item names answer the question and an exact live price does not. Knowledge mode returns instantly, costs nothing, and never opens a browser.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Restaurant name to look up (with `near` or `geo` for locality)."
    },
    "placeId": {
      "type": "string",
      "maxLength": 255,
      "description": "A specific location's place id, when one is already settled — from an earlier order preview or a location the user already picked. Skips the which-location question."
    },
    "near": {
      "type": "string",
      "maxLength": 300,
      "description": "The user's area, to pick the right location."
    },
    "geo": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number"
        },
        "lng": {
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false,
      "description": "Device coordinates — the strongest locality signal. If you do not have them and your client can request device location permission, do that first and pass the result here; otherwise use `near`."
    },
    "orderUrl": {
      "type": "string",
      "maxLength": 2000,
      "description": "A specific location's ordering URL from an earlier preview — skips name resolution."
    },
    "category": {
      "type": "string",
      "maxLength": 80,
      "description": "A section name from a previous 'categories' answer, EXACTLY as given — reads just that section."
    },
    "intent": {
      "type": "string",
      "maxLength": 400,
      "description": "The user's FULL request in their own words (e.g. 'what's under $2 at McDonald's'). Always pass it — it's used only if the live read fails, so the web-knowledge fallback answers exactly what they asked."
    },
    "mode": {
      "type": "string",
      "enum": [
        "live",
        "knowledge"
      ],
      "description": "Defaults to 'live' (reads their real site: ~a minute, a real browsing session). Pass 'knowledge' for RECOMMENDATIONS — 'what should I get at X', 'what are they known for' — where item names answer the question and an exact live price does not. Knowledge mode returns instantly, costs nothing, and never opens a browser."
    }
  },
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "status": {
      "type": "string",
      "description": "'menu' = a live read of their menu; present it as such. 'reading' = still being read; relay sayToUser ONLY if present (say nothing otherwise), then call again immediately with the same arguments — the call attaches to the read in flight and waits with it. 'categories' = their site keeps prices one level down; relay sayToUser and list the `categories` cleanly (short lines, no JSON) and, once the user picks, call again with the SAME arguments plus `category` set to their choice. A question that spans the WHOLE menu ('what is under $3?') CANNOT be answered from one section — say so plainly and let them pick a section to check, or offer to order a specific item directly. NEVER re-call this tool with the same arguments hoping for a different result: each call is a real, billed browse of their site, and a repeat gets you the same answer. 'unreadable' = Layout could not read the menu cleanly — do NOT invent or fill in items; relay sayToUser (ordering directly still works). 'knowledge' = the live menu couldn't be read, so `knowledge` holds a WEB-lookup answer to the user's request — relay sayToUser, present it as general/non-live background (prices vary, may be out of date), never as their live menu. 'needs_location' = ask the user which location, offering the `nearby` choices. Any other value: relay sayToUser."
    },
    "restaurant": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "address": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": true
    },
    "menu": {
      "type": "object",
      "properties": {
        "items": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "priceMinor": {
                "type": "number",
                "description": "Price in cents, from their site."
              },
              "description": {
                "type": "string"
              },
              "category": {
                "type": "string"
              },
              "optionGroups": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "What the restaurant calls this choice."
                    },
                    "required": {
                      "type": "boolean",
                      "description": "TRUE = their site will not let this item be ordered until it is answered."
                    },
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Their own option names. If you offer choices, offer THESE and nothing else."
                    },
                    "moreOptions": {
                      "type": "number",
                      "description": "Options beyond the ones listed — say the list is partial rather than implying it is all of them."
                    }
                  },
                  "additionalProperties": true
                },
                "description": "The choices this item carries, in the restaurant's words. THE FIELD BEING ABSENT IS NOT 'no choices': it means Layout could not read whether this item has any, so do not tell the user it needs none and do not invent a question. An EMPTY array does mean none."
              }
            },
            "required": [
              "name"
            ],
            "additionalProperties": true
          }
        }
      },
      "additionalProperties": true
    },
    "confidence": {
      "type": "string",
      "description": "'partial' = may not be the complete menu; say so when presenting."
    },
    "categories": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "On 'categories': the sections their site offers. Offer these to the user; pass their choice back as `category`."
    },
    "pricesUnavailable": {
      "type": "boolean",
      "description": "True = real items, but their site gave no prices. Do NOT supply prices from memory; say they weren't available."
    },
    "knowledge": {
      "type": "string",
      "description": "On 'knowledge': a NON-LIVE, web-lookup answer to the user's request. Relay via sayToUser; never present it as the live menu or as exact current prices."
    },
    "live": {
      "type": "boolean",
      "description": "false on a 'knowledge' answer — the info is general and may be out of date."
    },
    "sayToUser": {
      "type": "string",
      "description": "Layout's own wording for the user — relay it. On 'reading' this may be absent, which means stay quiet and just poll again."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

item_photo

Show what ONE menu item at a restaurant looks like, using the photo the restaurant itself published (screened, and never a stock or AI image). Read-only. Safe to repeat.

SessionCan call
OAuth connectionYes
Production build grantNo
Sandbox build grantNo

Input

ArgumentTypeRequiredDescription
itemstringYesThe menu item, in the user's words.
querystringNoRestaurant name (with `near` or `geo` for locality).
placeIdstringNoA location's place id already settled earlier in the thread.
nearstringNoThe user's area.
geoobjectNoDevice coordinates, when you have them.
orderUrlstringNoA location's ordering URL from an earlier preview.
storedOnlybooleanNoLeave unset. Layout's own follow-up on a pending photo.
labelstringNoLeave unset. Layout's own follow-up on a pending photo.
websitestringNoLeave unset. Layout's own follow-up on a pending photo.

The input JSON Schema:

{
  "type": "object",
  "properties": {
    "item": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120,
      "description": "The menu item, in the user's words."
    },
    "query": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Restaurant name (with `near` or `geo` for locality)."
    },
    "placeId": {
      "type": "string",
      "maxLength": 255,
      "description": "A location's place id already settled earlier in the thread."
    },
    "near": {
      "type": "string",
      "maxLength": 300,
      "description": "The user's area."
    },
    "geo": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number"
        },
        "lng": {
          "type": "number"
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false,
      "description": "Device coordinates, when you have them."
    },
    "orderUrl": {
      "type": "string",
      "maxLength": 2000,
      "description": "A location's ordering URL from an earlier preview."
    },
    "storedOnly": {
      "type": "boolean",
      "description": "Leave unset. Layout's own follow-up on a pending photo."
    },
    "label": {
      "type": "string",
      "maxLength": 120,
      "description": "Leave unset. Layout's own follow-up on a pending photo."
    },
    "website": {
      "type": "string",
      "maxLength": 2000,
      "description": "Leave unset. Layout's own follow-up on a pending photo."
    }
  },
  "required": [
    "item"
  ],
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#"
}

Output

The output JSON Schema. A successful call's structuredContent matches it.

{
  "type": "object",
  "properties": {
    "status": {
      "type": "string",
      "description": "'photo' = show `imageUrl` (a JPEG) with one short line naming the item; never describe it as something it is not. 'pending' = still being read; relay sayToUser and do NOT call again right away (wait for the user, or a minute). 'no_photo' / 'not_on_menu' / 'removed' / 'unavailable' = relay sayToUser and show nothing; never substitute another picture. 'needs_location' = ask which location from `nearby`. 'needs_item' = no dish was named; relay sayToUser and wait for one."
    },
    "restaurant": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "address": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "additionalProperties": true
    },
    "item": {
      "type": "string",
      "description": "The item's name as the restaurant writes it."
    },
    "priceMinor": {
      "type": "number",
      "description": "Their listed price in cents when last read; not a quote."
    },
    "imageUrl": {
      "type": "string",
      "description": "The photo, a JPEG up to 1000px on its long side."
    },
    "imageUrlWebp": {
      "type": "string",
      "description": "The same photo as WebP, smaller, for surfaces that take it."
    },
    "width": {
      "type": [
        "number",
        "null"
      ]
    },
    "height": {
      "type": [
        "number",
        "null"
      ]
    },
    "source": {
      "type": "string",
      "description": "'menu' = from their ordering menu; 'product_page' = from their own site's page for the item."
    },
    "checkedAt": {
      "type": [
        "string",
        "null"
      ],
      "description": "When the photo was last checked."
    },
    "sayToUser": {
      "type": "string",
      "description": "Layout's own wording; relay it."
    }
  },
  "additionalProperties": true,
  "$schema": "http://json-schema.org/draft-07/schema#"
}