Get a board
One board: its blocks, whether it is published, and its address.
The 91 tools Moonwalk gives Claude and ChatGPT, by area. This page is written from the list the connector serves, so a tool is here exactly when an agent can call it.
Each tool is labelled by what it does to your base:
Claude and ChatGPT ask you before any Changes tool.
Each description is the one the agent reads, word for word, so “you” in it is the agent.
Boards · Tables & data · Widgets · Automations · Files · Connections · Versions & trash · Moonwalk
One board: its blocks, whether it is published, and its address.
Every board of the base, in sidebar order, with whether each is published and its address.
A picture of a board's body as a visitor reads it — its text, pictures, value boxes and widgets, drawn from the rows they read now — 1280 pixels wide. A picture from a URL draws as not loaded. Whether the board is published changes nothing: it stays as it was. Nothing is written and no run is fired.
A board taller than 1200 pixels comes back one page at a time: the answer says page of pages, and page: 2 is the next 1200 pixels, and so on to the end. Each call draws the board afresh. A page past the last is refused, naming how many there are. theme picks light (the default) or dark. thrown is anything a widget threw while it drew. When it cannot be drawn the answer is {"screenshot": {"unavailable": <reason>}}.
Boards whose title holds q — the sidebar's filter.
Create a board, at the end of the list or after after. Every board is at the top level; one board links to another with a mention, never by holding it. It starts unpublished. The id is yours to mint.
Bring a board out of the trash, at the end of the list, as it was: a published one is served at its address again. list_trash names what is there.
Trash a board with everything on it. restore_board brings it back; list_trash with board shows it. While it is in the trash its address answers not-found. One with nothing in it that nothing but your own open sitting ever touched is deleted outright instead, and has no way back.
Apply a batch of block operations to one board — the only way to write, delete or reorder a block. One call is one transaction.
A heading is a heading block: its text, and level 1, 2 or 3 in props. A set at ["type"] turns a paragraph into a heading or back and keeps the block's id — send the level with it.
A value box is a text chunk {"value": {"sql": …, "description": …}}: one cell of a query, drawn live inside a sentence. Always both fields, the description one line. Rows on a board are a widget's job: make one with create_widget and put a widget block on the board.
A text chunk may instead be {"mention": "<board id>"}: a live board of this base, drawn as its current title. It stores no title, and anything else it names is refused with mention_invalid. A board is never a block of another board.
Marks sit flat on a chunk: bold, italic, underline, code, strike, link, and color for the ink and background for a wash behind it. A colour is a name, never a hex — grey, brown, orange, yellow, green, blue, purple, pink or red — and the two are independent.
An image block's props are exactly one of {"path": "/files/…"}, a picture in the base's files, or {"url": "https://…"}, one on the web. A path is copied into the block and shrunk to 4000 px, so moving or deleting the file later changes nothing; it is stored as image, width and height, and the answer's stored says so. A URL is drawn live and breaks when its site does. PNG, JPEG, GIF and WebP only. To put a local file on a board, request_upload it and name the path you get back.
The description is for the person, not for you: what the query asks, in the words someone would use to ask for it — "Tasks still open, newest first". The SQL is physical names in hex and nobody can read it, so this is the only readable record of what the block draws, and a block saved without one is refused.
Lock a board, or unlock it. A locked board refuses every edit to its title and its body — edit_board answers board_locked — for everyone, until somebody unlocks it. Moving, trashing and publishing it still work. Unlock only when the person asked you to change that board.
Put a board after after in the list, or first when after is null.
Publish a board, or unpublish it. Published, anyone with its link reads it at /public/<name>-<token>, signed in or not, and it is listed on the base's Public home. The answer's path is that link. The first publish mints the token, and it never changes after: unpublishing stops serving it, and publishing again brings the same one back. Only the token is read, so a rename never breaks a link already handed out. Publish only when the person asked for the board to be public.
One table and every column on it, with each column's type.
Rows of one table, by limit and offset. To filter, sort, group or join, write SQL — run_sql. The database screen's own filter is the UI's and adds nothing an agent holding SQL does not already have.
Every live table in the base.
Run read-only SQL against this base's tables. Ordinary Postgres — joins, GROUP BY, date_trunc, window functions, now().
Names are physical and derived from ids, so a rename never breaks a query: a table is the view "v_<hex>" (get_table gives the ids; the hex is the uuid without dashes) and a column is "c_<hex>". Select id when you want the rows to be writable or numbered. A select/multi_select value is an option id, not its label.
Writes are refused by the database itself, not by a check here: there is one role per base, granted SELECT on its views and nothing else. Use write_rows to change anything.
Add a column to a table.
Create a table and its columns.
Take a dropped column back under the same id, with every value it held. Every stored query naming it runs again with no edit.
Bring a trashed table back, if its name is still free.
Change a column's type. Refused, naming the rows that stopped it, when a value would not survive the cast.
Drop a column. Never refused — every query naming it stops running and says so. restore_column takes it back, with its values — and so do undo, to the place it held, and reverting the table to a version from before the drop.
Trash a table. restore_table brings it back — unless the table held no rows and nothing but your own open sitting ever touched it, which is deleted outright and has no way back.
Rename a column. A label edit — nothing in the database moves.
Rename a table. Its columns and rows are untouched.
Insert, set, delete or restore rows of one table. One call is one transaction and one undo step, however many rows it touches. A new row lands last; rows are in # order and cannot be placed.
One widget: its declaration, its screen source and its build hash. An empty source is a stub someone made from the menu — a name waiting for you to write its screen with update_widget.
Run every data query a widget declares and return the rows.
What went wrong with a widget, newest first, each with its kind: undeclared_data and undeclared_run for a read or run its declaration never named, automation_gone for a declared run whose automation has left, render for a screen that threw, compile for a save the compiler refused. A run's own failure is not here — get_run has it. limit is capped at 200.
Every live widget in the base, by name and size.
A picture of a widget as it looks now, drawn from the rows it reads now — the same picture, four sizes and thrown a save answers with, without saving. For a widget whose rows may have changed since it was last saved; a save already answers with one. Nothing is written and no run is fired: while it draws, every run is refused not while previewing. theme picks light (the default) or dark. A stub answers {"screenshot": null}, and one that cannot be drawn {"unavailable": <reason>}.
Create a widget from a declaration and a TypeScript screen. The screen is compiled on the way in; a refusal names every violation by line.
Say why in change_description — one line, in words the person would use: "Shows the week's total above the chart". It is the line the widget's history shows for this version, and a source saved without one is refused (422 change_description_required).
A screen imports react and @moonwalk/widget and nothing else, and exports its component as the default export:
import { publicUrl, useFocus, useQuery, useRun, useRunResult, useScroll, useTheme } from '@moonwalk/widget'
export default function Tasks() {
const tasks = useQuery('tasks') // a key of declaration.data
const move = useRun('move') // a key of declaration.runs
const collect = useRunResult() // a run answered `strt`, asked again by its id
const theme = useTheme() // 'light' or 'dark'
const scroll = useScroll() // 0 → 1 as the page scrolls this block past
const focus = useFocus() // false while the reader is in another window
if (!tasks.rows) return <p>Loading…</p>
return (
<>
<audio src={publicUrl('theme.mp3')} controls /> {/* /files/Public/theme.mp3 */}
<ul>{tasks.rows.map((t) => <li key={t.id}>{String(t.title)}</li>)}</ul>
</>
)
}
The <Screen> provider is mounted around it for you; a screen never renders one. useQuery gives {rows, broken} and nothing that writes. There is no fetch, no token and no storage — the broker is the only way in or out.
useRun gives a function resolving to {run_id, status, returncode, output}. status is fin (it ended) or strt (still going past the server's 3-second wait — not a failure). fin does not mean it worked: check status === 'fin' && returncode === 0. rej never reaches a screen — a refused run rejects the promise instead.
output is the JSON object the automation's last step returned — its ReturnType — so read the field you want: run.output.message. It is null while strt and when the last step failed, and past 16 KB it is {"$over": <bytes>}: a run answers, it does not hand over a dataset. get_widget reports each run's output_schema. A run answered strt can be collected later: useRunResult()(run.run_id) resolves the same shape.
A run may take a file. A trigger property marked {"type": "string", "format": "file"} takes one, and a screen passes a File at the top level of input — useRun('import')({ book: file, language: 'en' }). It goes as bytes in the one request, is saved into /files/Uploads/, and the step is handed the path it was saved at. Nothing in the SDK is new for it: an <input type="file"> and the File it yields.
This is the one thing you cannot do yourself — fire_automation names a path because MCP carries no bytes, and the browser is the caller that sends them. A file past 100 MB is refused, 413 too_large.
A screen loads a file from the base's Public/ folder by naming it: <audio src={publicUrl('theme.mp3')} controls /> plays /files/Public/theme.mp3. Images and fonts load the same way. Only files in Public/ can be named like that, and anybody can open them, signed in or not, so moving a file in is publishing it. The screenshot a save answers with does not load them.
A declaration is data and runs. A widget reads and runs, and does nothing else — there is no way to write a row through a query, so a data entry has no mode and a declaration carrying one is refused.
Each declaration.data entry is two fields: sql and description.
The SQL is read-only Postgres over this base's views — "v_<hex>" for a table and "c_<hex>" for a column, both derived from ids, so a rename never breaks one. Alias every column to the key the screen reads, and select id when the screen needs a row to name in a run.
The description is for the person, not for you: what the query asks, in the words someone would use to ask for it. The SQL is hex and nobody can read it, so this is the only readable record, and an entry without one is refused.
Each declaration.runs entry is the screen's name for it → the id of an automation that already exists in this base. Its trigger must be manual: a cron or webhook trigger fills its own payload, so a screen has nothing to supply. useRun('move')(input) fires it with input as the trigger's output, so input has to fit that trigger's output_schema — read it back off get_widget, which reports each run as {automation_id, automation_name, input_schema, output_schema}, the last being what the run's output holds. Build the automation first, then bind it; there is no way to make one from here.
The box is fixed. A screen draws into exactly width × height CSS pixels and nothing grows to fit it: anything taller scrolls inside the box, so design to the number. size is its own field beside the declaration — {"width": 480, "ratio": "4:3"} — and a resize is that alone. A new widget with a screen names its size (422 size_required without one); only a stub is given the smallest. The 16 boxes:
320 wide: 16:9 is 320×180, 4:3 is 320×240, 3:4 is 320×427, 2:3 is 320×480
480 wide: 16:9 is 480×270, 4:3 is 480×360, 3:4 is 480×640, 2:3 is 480×720
640 wide: 16:9 is 640×360, 4:3 is 640×480, 3:4 is 640×853, 2:3 is 640×960
960 wide: 16:9 is 960×540, 4:3 is 960×720, 3:4 is 960×1280, 2:3 is 960×1440
Every save that changes the screen, the declaration or the size answers with a screenshot of the widget as drawn, from its real rows, and four sizes: clientWidth and clientHeight are the box, scrollWidth and scrollHeight the content, the page's or a div's that fills the box and scrolls or clips, whichever is bigger. A scrollHeight above clientHeight means the content scrolls inside the box. thrown is every message the screen threw while it drew, word for word. While it draws, every run is refused — not while previewing — so a screenshot never writes to the base. theme picks light (the default) or dark. When it cannot be drawn the save still stands, and screenshot is {"unavailable": <reason>}.
Bring a trashed widget back.
Trash a widget. restore_widget brings it back. Refused while a board still draws it, naming the blocks. One with no source that nothing but your own open sitting ever touched is deleted outright instead, and has no way back.
Replace a widget's name, declaration, source or size — any of them in one save. Each is replaced whole, never merged.
Say why in change_description — one line, in words the person would use: "Shows the week's total above the chart". It is the line the widget's history shows for this version, and a source saved without one is refused (422 change_description_required).
useRun gives a function resolving to {run_id, status, returncode, output}. status is fin (it ended) or strt (still going past the server's 3-second wait — not a failure). fin does not mean it worked: check status === 'fin' && returncode === 0. rej never reaches a screen — a refused run rejects the promise instead.
output is the JSON object the automation's last step returned — its ReturnType — so read the field you want: run.output.message. It is null while strt and when the last step failed, and past 16 KB it is {"$over": <bytes>}: a run answers, it does not hand over a dataset. get_widget reports each run's output_schema. A run answered strt can be collected later: useRunResult()(run.run_id) resolves the same shape.
A run may take a file. A trigger property marked {"type": "string", "format": "file"} takes one, and a screen passes a File at the top level of input — useRun('import')({ book: file, language: 'en' }). It goes as bytes in the one request, is saved into /files/Uploads/, and the step is handed the path it was saved at. Nothing in the SDK is new for it: an <input type="file"> and the File it yields.
This is the one thing you cannot do yourself — fire_automation names a path because MCP carries no bytes, and the browser is the caller that sends them. A file past 100 MB is refused, 413 too_large.
A screen loads a file from the base's Public/ folder by naming it: <audio src={publicUrl('theme.mp3')} controls /> plays /files/Public/theme.mp3. Images and fonts load the same way. Only files in Public/ can be named like that, and anybody can open them, signed in or not, so moving a file in is publishing it. The screenshot a save answers with does not load them.
A declaration is data and runs. A widget reads and runs, and does nothing else — there is no way to write a row through a query, so a data entry has no mode and a declaration carrying one is refused.
Each declaration.data entry is two fields: sql and description.
The SQL is read-only Postgres over this base's views — "v_<hex>" for a table and "c_<hex>" for a column, both derived from ids, so a rename never breaks one. Alias every column to the key the screen reads, and select id when the screen needs a row to name in a run.
The description is for the person, not for you: what the query asks, in the words someone would use to ask for it. The SQL is hex and nobody can read it, so this is the only readable record, and an entry without one is refused.
Each declaration.runs entry is the screen's name for it → the id of an automation that already exists in this base. Its trigger must be manual: a cron or webhook trigger fills its own payload, so a screen has nothing to supply. useRun('move')(input) fires it with input as the trigger's output, so input has to fit that trigger's output_schema — read it back off get_widget, which reports each run as {automation_id, automation_name, input_schema, output_schema}, the last being what the run's output holds. Build the automation first, then bind it; there is no way to make one from here.
The box is fixed. A screen draws into exactly width × height CSS pixels and nothing grows to fit it: anything taller scrolls inside the box, so design to the number. size is its own field beside the declaration — {"width": 480, "ratio": "4:3"} — and a resize is that alone. A new widget with a screen names its size (422 size_required without one); only a stub is given the smallest. The 16 boxes:
320 wide: 16:9 is 320×180, 4:3 is 320×240, 3:4 is 320×427, 2:3 is 320×480
480 wide: 16:9 is 480×270, 4:3 is 480×360, 3:4 is 480×640, 2:3 is 480×720
640 wide: 16:9 is 640×360, 4:3 is 640×480, 3:4 is 640×853, 2:3 is 640×960
960 wide: 16:9 is 960×540, 4:3 is 960×720, 3:4 is 960×1280, 2:3 is 960×1440
Every save that changes the screen, the declaration or the size answers with a screenshot of the widget as drawn, from its real rows, and four sizes: clientWidth and clientHeight are the box, scrollWidth and scrollHeight the content, the page's or a div's that fills the box and scrolls or clips, whichever is bigger. A scrollHeight above clientHeight means the content scrolls inside the box. thrown is every message the screen threw while it drew, word for word. While it draws, every run is refused — not while previewing — so a screenshot never writes to the base. theme picks light (the default) or dark. When it cannot be drawn the save still stands, and screenshot is {"unavailable": <reason>}.
One run: its status, its timings, what it received (input) and what it returned (output). Either is {"$over": <bytes>} past 16 KB.
One row per executed step, in execution order — what you debug a run from. cpu_ms and wall_ms are what the CPU and wall-time limits count: every process it started, from the moment it was sent. Installing its dependencies comes first and is in neither, so a first run can take a minute and report a few hundred ms.
What a step declares as config, and the values saved against it.
What a step declares as connections, and which base connection is bound to each key.
The Python source of one step, as it is on disk.
One automation: its step tree, its trigger and its state.
Where the person gets a webhook trigger's signing secret, never the secret itself. Show them the url: on that screen they press the eye on the trigger and copy the secret to paste upstream. secret_set is false while an upstream secret has not been pasted in.
Where the person pastes the upstream's signing secret, never the secret itself. Show them the url: on that screen they paste it into the trigger; never ask for it in the conversation. Refused (409 system_generated) when Moonwalk made the secret; that one is copied, with get_webhook_secret.
An automation's runs, newest first.
Every step of an automation, in tree order, with its inputs.
Every automation in the base, with its steps and trigger.
Add a step. The source is linted at save: it must define a top-level run(...) and, for a code step, a ReturnType model it returns.
Say why in change_description — one line a person would understand: "Skips weekends". It is the line the automation's history shows, and code saved without one is refused (422 change_description_required).
A positional parameter of run(...) is an input, filled from the step's inputs list in order. {"step": "trigger", "path": [...]} reads what fired the run; {"step": "<hash>", "path": [...]} reads an earlier step's output.
def run(title, file) -> ReturnType: ...
inputs=[{"step": "trigger", "path": ["title"]},
{"step": "trigger", "path": ["file"]}]
A count that does not match, or the same names in a different order, is refused at save (422 invalid_step).
A step reaches the base's own tables through the SDK — no connection needed, and tables and columns are named by id, never by name:
from mw_sdk import BaseModel, Config, Annotated, Literal, Ge, Le, Gt, Lt, MinLen, MaxLen, MultipleOf, connection_type, SecretConnection, BasicConnection, HmacConnection, AccountConnection, tables, files
import uuid
class ReturnType(BaseModel):
written: int
def run() -> ReturnType:
tables.write(table_id, [
{"op": "insert", "row": str(uuid.uuid4()), "values": {column_id: value}},
])
return ReturnType(written=1)
tables.sql, tables.columns and tables.rows read; tables.write writes, and every write a run makes lands in one transaction, the run's. Undo never takes it back; on each table it touches it is a version of its own. discard_latest_version takes it back while it is still the newest, revert_version to the version before it once something is on top. An op is one of insert, set, delete, restore; row is a UUID the caller mints; values is keyed by column id. A new row lands last; rows are in # order and cannot be placed.
tables.sql is read-only Postgres over the base's views: a table is "v_<hex>" and a column "c_<hex>", the uuid without its dashes, so a rename never breaks a step. tables.columns(table_id) is how a step gets from a name to an id at run time rather than pasting one in.
A step reads and writes files under /files, its working directory, with plain open. Pass any file name that came from outside — a trigger's input, a cell — through files.inside(folder, name) before opening it: it answers the absolute path, /files/<folder>/<name>, and raises ValueError for a name that climbs out of folder. A file field ("format": "file" on a manual trigger) hands the step the path the file was saved at, /files/Uploads/<name>.
To delete a file, files.trash(path): it answers nothing, and the file waits in the base's trash for 30 days, so a mistake can be taken back. os.remove cannot be.
A config (Config[T] on run) is a setting and never a credential. A key, password or token is a connection, asked for with request_connection.
Each step gets 1 hour of wall time and 120 CPU-seconds, counted across every process it starts. Past either it is killed, and its step run's error_type says which: wall_time or cpu_time.
Create an automation. The trigger's mode is set here and never again — a different mode is a different automation.
Bring a trashed automation back, its trigger live again.
Lock this exact version and allow it to fire. Needs steps — the version is always there, because every edit goes into one.
Drop one config value, back to the declared default.
Stop it firing and unlock it for editing.
Delete a step and everything under it — an if takes both branches. Refused while a step left behind reads one that goes; rewire or delete that step first.
Trash an automation. Its steps, runs, versions and files are kept and nothing fires it; restore_automation brings it back with its trigger live. Refused while a widget binds it, naming the widgets. One with no steps that nothing but your own open sitting ever touched is deleted outright instead, and has no way back.
Fire an automation by hand, whether it is on or off. Returns the run id at once — read the run to find out what happened. A file field ("format": "file") takes the path of a file already in the base — /files/Uploads/book.pdf; request_upload is how a file from your own machine gets there. You are the one caller that names a path; every other sender of a run, a widget's screen included, sends the file itself. 422 file_not_found for a path that is not a file. Each step gets 1 hour of wall time and 120 CPU-seconds, counted across every process it starts. Past either it is killed, and its step run's error_type says which: wall_time or cpu_time.
Run a Python snippet in this base's sandbox and return its exit code, stdout and stderr. dependencies are pip requirements installed into a fresh venv; the first call for a given set takes about a minute. A snippet gets the same limits as a step: 1 hour of wall time and 120 CPU-seconds, counted across every process it starts. Past either it is killed, and killed_by says which: wall_time or cpu_time. cpu_ms and wall_ms are what the CPU and wall-time limits count: every process it started, from the moment it was sent. Installing its dependencies comes first and is in neither, so a first run can take a minute and report a few hundred ms.
Set one config value, checked against the step's declaration. A config (Config[T] on run) is a setting and never a credential. A key, password or token is a connection, asked for with request_connection.
Replace a step's name, source, dependencies or inputs. Say why in change_description — one line a person would understand: "Skips weekends". It is the line the automation's history shows, and code saved without one is refused (422 change_description_required). Each step gets 1 hour of wall time and 120 CPU-seconds, counted across every process it starts. Past either it is killed, and its step run's error_type says which: wall_time or cpu_time.
Edit the trigger within its mode: output schema, run-record limit, and that mode's own field. The mode itself is fixed, and there is no on/off here: whether it fires is activate_automation.
Rename an automation or change its description.
Find files and folders by a glob, without listing folders one by one. A pattern with no / matches a name at any depth (*.md); one with a / matches the path from path, ** descending (notes/**/*.md), or from the root when it starts /files/. Dotfiles are included. At most 100 answers. Documented at https://docs.moonwalk.now/tools.html#files.
One directory of the base's tree. A path is absolute from /files, the root — /files/notes/todo.md — and every answer spells it that way. Documented at https://docs.moonwalk.now/tools.html#files.
A file's text. An image comes back as a picture you can see: encoding image, and the picture, shrunk to 2000 px. Any other file that is not UTF-8, or is too large, comes back as binary with no text. Documented at https://docs.moonwalk.now/tools.html#files.
Find lines matching a regular expression (Python re) inside the base's files, dotfiles included, and answer each hit's path, line number and text. Case-sensitive unless case_insensitive. path is the folder to search under; glob narrows which files are read, matched the way search_files matches. Binary files, files too large to preview and lines over 10,000 characters are skipped. At most 100 hits; truncated says there were more. Documented at https://docs.moonwalk.now/tools.html#files.
Get a one-time link for sending a file from your own machine into the base — MCP carries no bytes, so this is how one gets in. Run the command it answers with your file's path in place of the placeholder; the upload answers {"path": "/files/Uploads/…"}, which is what a file field in fire_automation takes. The link works once, for 5 minutes. If the upload cannot connect, do what the note says.
Make one directory, whose parent must already be there.
Put a trashed entry back where it came from, under the name it had. id is a row's from list_trash, not a path. Refused when something is at that path now.
Move a file, or a directory and everything in it, into the base's trash, where it waits 30 days. It leaves the tree — list_trash with kind="file" is where it is now, and restore_file takes the id from there and puts it back where it was. Never the root, /files/Uploads or /files/Public. Nothing you do deletes a file for good. Documented at https://docs.moonwalk.now/tools.html#files.
Replace old with new in a text file, leaving the rest of it byte for byte. old must appear exactly once — include enough of the lines around it to make it unique — unless replace_all. Nothing changes when the edit is refused, and the answer says why. To write a whole file, use write_file. Documented at https://docs.moonwalk.now/tools.html#files.
Move or rename an entry. to is the whole new path. Never /files/Uploads or /files/Public itself, and never in or out of the trash.
Write a whole text file, making or replacing it. The parent must exist, except under /files/.agent/memory/, whose folders are made.
How an ask was answered: pending or filled, and — once filled — the connection that was actually made, which may not be the name you asked for. 404 no_such_request once the person declined it, expired once the hour is up.
One action of a linked account: its argument JSON Schema in input_parameters, plus its output shape and its scopes. Read it before writing the call rather than guessing at argument names.
What a linked account can do: every action slug a step may call through it, narrowed by search. 409 not_an_account for a connection that was typed rather than clicked — only an account has a catalogue.
Every ask in the base still waiting on the person — yours, another chat's, or one made over /api/mcp — with its name, kind, who asked, why, and when it runs out (expires_at). The list the Connections dot reads.
Every connection the base holds. The fields never come back out.
The services an account can be linked for, searched by name — the Connections screen's picker. Look the slug up here before request_connection with account rather than guessing it. 503 not_configured means no account can be linked on this stack at all.
Ask the person for a credential you must never see. Name the connection you want and why: secret, basic or hmac for a key they type, or account for a service they connect by clicking — then provider is the service's slug, like gmail. Never ask for an API key, password or token in the conversation, and never accept one there. In the chat panel a box asks them above the composer and your turn waits: what comes back is their answer, {outcome, name, message?} — filled (bind to name), denied (carry on without it and don't ask in the conversation), asked (they want to know why first: answer message, then ask again if you still need it) or expired. Anywhere else, show them the URL this answers and stop; then poll get_connection_request. The name is a proposal — bind steps to the name the answer gives, not the one you asked for. For a key, how_to_get is required: the path to it from where the person starts, as site > page > button, like platform.openai.com > API keys > Create new secret key. Add how_to_get_url, like https://platform.openai.com/api-keys, when you know the page. It shows in the box where they type the key, so write it for someone who has never done it. When you're not sure of the path, say so in it rather than inventing one. An account takes neither.
Bind a base connection to one declared key.
Delete a connection. Refused while a step is bound to it.
Unbind whatever is on one declared key.
Take a pending ask back, when the person asked you to drop it or you no longer need it. It removes any pending ask in the base, not only your own — list_connection_requests says who asked. Nothing is woken, not even the chat that asked: it learns from get_connection_request's 404, the way it learns of an expiry. 409 already_answered once filled.
One version, and what it changed against the version before it.
What one section has in its trash, newest first, each with when it goes for good (purge_at, 30 days after it was deleted). file lists a deleted folder and not what was in it, and each row carries the path it came from — which is what tells two files of one name apart. Restore with restore_board, restore_widget, restore_automation, restore_table or restore_file, each of which takes the id of a row here.
A board's, a table's, a widget's or an automation's versions, newest first. One writer's writes inside ten minutes of each other are one version, and each row's actor_kind and via say whose: you, the person, or a run.
What undo would take back, without taking it: the transaction, how many rows, and how many changes of each kind — drop or add for a column. undo takes the newest write of the member you act for — theirs in the browser as much as yours — so read this first when that matters. {"ok": true} when there is nothing to undo.
Throw away the newest version and put the resource back as the version before had it — yours, your person's or a run's. Appends nothing. Only you can do this: a person reverts instead. Refused when a run names the version (409 version_has_runs) and when it is the only one (409 only_version).
Put the resource back as an older version had it, as a new version — nothing is removed. A table goes back whole, other people's later writes included. An automation's configs and bindings do not survive it, and it is refused while the automation is active (409 locked).
Take back the newest transaction of the member you act for — one this session just made, or one they made in the browser; next_undo says which before you call it. A row write, or a column dropped or added: a dropped column comes back where it was, with its values. Cells another write has since changed are skipped and reported, never overwritten.
The Moonwalk bases you can reach, as {id, name}, oldest first. Every other tool but read_overview acts on one of them and takes its id as base_id. More than one base means the person's words may fit either.
How a Moonwalk base works: the rules no single tool states.
Which screen the person last had open in this base's web app: its section (boards, database, widgets, …), the id and name of what was open, and line, one sentence saying so. seconds_ago is how long since the tab reported it — it reports on moving and on coming back into view, so an old one most likely means the person has moved on to another window. {"screen": null} means this person has never had the base open.