Moonwalk Docs

Tools

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

Boards

Get a board

get_board Reads

One board: its blocks, whether it is published, and its address.

List boards

list_boards Reads

Every board of the base, in sidebar order, with whether each is published and its address.

Look at a board

screenshot_board Reads

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.

More

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>}}.

Search boards

search_boards Reads

Boards whose title holds q — the sidebar's filter.

Create a board

create_board Adds

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.

Restore a board

restore_board Adds

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.

Delete a board

delete_board Changes

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.

Edit a board

edit_board Changes

Apply a batch of block operations to one board — the only way to write, delete or reorder a block. One call is one transaction.

More

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 or unlock a board

set_board_lock Changes

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.

Move a board

move_board Changes

Put a board after after in the list, or first when after is null.

Publish or unpublish a board

set_board_published Changes Reaches outside

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.

Tables & data

Get a table

get_table Reads

One table and every column on it, with each column's type.

List a table's rows

list_rows Reads

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.

List tables

list_tables Reads

Every live table in the base.

Run read-only SQL

run_sql Reads

Run read-only SQL against this base's tables. Ordinary Postgres — joins, GROUP BY, date_trunc, window functions, now().

More

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

add_column Adds

Add a column to a table.

Create a table

create_table Adds

Create a table and its columns.

Restore a column

restore_column Adds

Take a dropped column back under the same id, with every value it held. Every stored query naming it runs again with no edit.

Restore a table

restore_table Adds

Bring a trashed table back, if its name is still free.

Change a column's type

change_column_type Changes

Change a column's type. Refused, naming the rows that stopped it, when a value would not survive the cast.

Delete a column

delete_column Changes

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.

Delete a table

delete_table Changes

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

rename_column Changes

Rename a column. A label edit — nothing in the database moves.

Rename a table

rename_table Changes

Rename a table. Its columns and rows are untouched.

Write rows

write_rows Changes

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.

Widgets

Get a widget

get_widget Reads

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.

Get a widget's data

get_widget_data Reads

Run every data query a widget declares and return the rows.

List a widget's errors

list_widget_errors Reads

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.

List widgets

list_widgets Reads

Every live widget in the base, by name and size.

Look at a widget

screenshot_widget Reads

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

create_widget Adds

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.

More

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>}.

Restore a widget

restore_widget Adds

Bring a trashed widget back.

Delete a widget

delete_widget Changes

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.

Update a widget

update_widget Changes

Replace a widget's name, declaration, source or size — any of them in one save. Each is replaced whole, never merged.

More

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>}.

Automations

Get a run

get_run Reads

One run: its status, its timings, what it received (input) and what it returned (output). Either is {"$over": <bytes>} past 16 KB.

Get a run's steps

get_run_steps Reads

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.

Get a step's config values

get_step_configs Reads

What a step declares as config, and the values saved against it.

Get a step's connections

get_step_connections Reads

What a step declares as connections, and which base connection is bound to each key.

Get a step's source

get_step_source Reads

The Python source of one step, as it is on disk.

Get an automation

get_automation Reads

One automation: its step tree, its trigger and its state.

Link to a webhook's secret

get_webhook_secret Reads

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.

Link to paste a webhook secret

set_webhook_secret Reads

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.

List an automation's runs

list_runs Reads

An automation's runs, newest first.

List an automation's steps

list_steps Reads

Every step of an automation, in tree order, with its inputs.

List automations

list_automations Reads

Every automation in the base, with its steps and trigger.

Create a step

create_step Adds

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.

More

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

create_automation Adds

Create an automation. The trigger's mode is set here and never again — a different mode is a different automation.

Restore an automation

restore_automation Adds

Bring a trashed automation back, its trigger live again.

Activate an automation

activate_automation Changes

Lock this exact version and allow it to fire. Needs steps — the version is always there, because every edit goes into one.

Clear a step config value

clear_step_config Changes

Drop one config value, back to the declared default.

Deactivate an automation

deactivate_automation Changes

Stop it firing and unlock it for editing.

Delete a step

delete_step Changes

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.

Delete an automation

delete_automation Changes

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

fire_automation Changes Reaches outside

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 code in the sandbox

execute_code Changes Reaches outside

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 a step config value

set_step_config Changes

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.

Update a step

update_step Changes

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.

Update a trigger

update_trigger Changes

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.

Update an automation

update_automation Changes

Rename an automation or change its description.

Files

Find files by name

search_files Reads

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.

List files

list_files Reads

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.

Read a file

read_file Reads

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.

Search inside files

grep Reads

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 an upload link

request_upload Adds

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 a directory

mkdir Adds

Make one directory, whose parent must already be there.

Restore a file

restore_file Adds

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.

Delete a file

delete_file Changes

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.

Edit a file

edit_file Changes

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 a file

move_file Changes

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 file

write_file Changes

Write a whole text file, making or replacing it. The parent must exist, except under /files/.agent/memory/, whose folders are made.

Connections

Get a connection request

get_connection_request Reads

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.

Get an account action

get_account_action Reads

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.

List an account's actions

list_account_actions Reads

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.

List connection requests

list_connection_requests Reads

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.

List connections

list_connections Reads

Every connection the base holds. The fields never come back out.

List linkable services

list_services Reads

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 for a connection

request_connection Adds

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 connection to a step

bind_step_connection Changes

Bind a base connection to one declared key.

Delete a connection

delete_connection Changes

Delete a connection. Refused while a step is bound to it.

Unbind a step's connection

unbind_step_connection Changes

Unbind whatever is on one declared key.

Withdraw a connection request

withdraw_connection_request Changes

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.

Versions & trash

Get a version

get_version Reads

One version, and what it changed against the version before it.

List the trash

list_trash Reads

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.

List versions

list_versions Reads

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.

Preview the next undo

next_undo Reads

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.

Discard the latest version

discard_latest_version Changes

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).

Revert to a version

revert_version Changes

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).

Undo the last change

undo Changes

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.

Moonwalk

List your bases

list_bases Reads

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.

Read the base's rules

read_overview Reads

How a Moonwalk base works: the rules no single tool states.

Read where the person is

read_screen Reads

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.