# Field24 MCP server docs Every page of https://field24.ai/docs/mcp in one file. The index is https://field24.ai/llms.txt. --- # Field24 MCP server URL: https://field24.ai/docs/mcp Connect your AI assistant to Field24. Attach a set of plans, ask for an estimate, and Field24 measures the takeoff, prices it from your own rates and local store prices, and builds a proposal, all in your Field24 account. Your assistant starts the work, checks on it and walks you through the results. > **Using ChatGPT?** Install the Field24 plugin from the ChatGPT directory instead of adding the server by hand: the plugin ships the estimating skills that tell ChatGPT how to run a takeoff, review it and price it. A bare MCP connection gets the tools without the playbook. ## Quickstart 1. **Connect** your assistant to `https://field24.ai/mcp` (steps for each app below). 2. **Sign in** when Field24's page opens: enter your phone number and the code we text you. New to Field24? Your account is created on the spot. 3. **Attach your plans and ask.** Start with one of these: - "Create an estimate based on this set of plans" - "Do a takeoff for this project" - "Price my takeoff and build a proposal I can send" A takeoff takes 15 to 30 minutes. You don't have to wait in the chat: your assistant checks back and tells you when it's done. ## Connect your assistant ### ChatGPT (recommended) Open the Field24 plugin in the ChatGPT directory, choose **Add**, and sign in with your Field24 account. ### Claude 1. Open **Settings → Connectors**. 2. Choose **Add custom connector**. 3. Paste `https://field24.ai/mcp`, then connect and sign in with your Field24 account. ### Claude Code ```bash claude mcp add --transport http field24 https://field24.ai/mcp ``` Then run `/mcp` in Claude Code and sign in. ### Cursor Add Field24 to `~/.cursor/mcp.json`, then sign in when Cursor asks: ```json { "mcpServers": { "field24": { "url": "https://field24.ai/mcp" } } } ``` ### VS Code Add Field24 to `.vscode/mcp.json`, then start the server and sign in: ```json { "servers": { "field24": { "type": "http", "url": "https://field24.ai/mcp" } } } ``` ### Codex ```bash codex mcp add field24 --url https://field24.ai/mcp codex mcp login field24 ``` ### Other apps Any app that supports remote MCP servers with sign-in works: add `https://field24.ai/mcp` as a server and sign in with your Field24 account when it asks. ## What it can do - Create a project for each job and hold its plans, drawings and photos. See [Files](https://field24.ai/docs/mcp/files.md). - Measure a full quantity takeoff from the plans, have Field24's reviewer check it, and fix what the review finds. See the [Estimating flow](https://field24.ai/docs/mcp/estimating-flow.md). - Price the takeoff from your price book and local store prices, then review the pricing. - Build a branded, e-signable PDF proposal. - Read and update your price book: your labor and material rates. It works only on your own Field24 account, the same one you use at `https://field24.ai/app`. Starting a takeoff, pricing or a proposal needs a Field24 free trial or subscription; projects, files and your price book work without one. ## The server at a glance | | | | --- | --- | | URL | `https://field24.ai/mcp` | | Sign-in | OAuth with your Field24 account (phone number and a texted code). See [Authentication](https://field24.ai/docs/mcp/authentication.md). | | Apps | ChatGPT (plugin), Claude, Claude Code, Cursor, VS Code, Codex and other MCP clients | | Server card | [`/.well-known/mcp/server-card.json`](https://field24.ai/.well-known/mcp/server-card.json) | ## Need help? Use the [support form](https://field24.ai/support) for help with an estimate, your account or billing. The [Tool reference](https://field24.ai/docs/mcp/tools.md) lists everything your assistant can call, and [Errors and limits](https://field24.ai/docs/mcp/errors-and-limits.md) explains the messages you might see. --- # Authentication URL: https://field24.ai/docs/mcp/authentication You connect with your own Field24 account. There is no API key to copy: the first time your assistant uses Field24, it opens Field24's sign-in page, and everything it does afterwards happens in that account. ## Signing in 1. Your assistant opens Field24's sign-in page in your browser. 2. Enter your phone number and the code we text you. A number Field24 has not seen gets a new account on the spot. 3. You're sent back to your assistant, connected. The sign-in link is good for 15 minutes. If it expires, start the connection again from your assistant. ## What your assistant can do with the connection Signing in lets your assistant, on your account only: - see your profile, usage and credit balance; - list, create and update your projects and add files to them; - read and change your price book; - start, check, steer and stop takeoffs, pricing and proposals, and download the proposals. It cannot reach any other account or your payment details. ## Staying signed in Your assistant renews its access on its own; every few days you'll be asked to sign in again. If your assistant says Field24 needs a permission it doesn't have, reconnect Field24 and approve it. To disconnect, remove the Field24 connector or plugin in your assistant's settings. ## Building your own client The server uses the standard MCP authorization flow: OAuth 2.1 with PKCE, discovered from the server's first `401` response, with dynamic client registration. Any MCP client that supports remote servers with OAuth signs in on its own; there is nothing to register with Field24 beforehand. Send the access token as `Authorization: Bearer ` on every request. --- # Estimating flow URL: https://field24.ai/docs/mcp/estimating-flow An estimate is a few jobs that run one after another in your Field24 account: a measured takeoff, a review of it, pricing, a review of the pricing, and an optional proposal. Your assistant starts each one, checks on it, and brings you the results. It never measures or prices anything itself. ## What happens 1. **Project.** Your assistant creates a project for the job and adds your plans, drawings or photos to it. See [Files](https://field24.ai/docs/mcp/files.md). 2. **Takeoff.** Field24 reads the plans and measures every item in the scope you asked for, row by row, by CSI division. 3. **Takeoff review.** Field24's reviewer checks the takeoff against the plans. Corrections backed by the plans are applied without waiting on you; you're asked only what the plans don't settle, each with a suggested answer. 4. **Pricing.** Field24 prices the reviewed takeoff from your price book first, then local store prices and market labor rates. 5. **Pricing review.** Field24's reviewer checks the prices, and its corrections are applied the same way. 6. **Proposal (optional).** Field24 builds a branded, e-signable PDF. Your assistant can also write a proposal in the chat from the priced rows instead. Your assistant doesn't ask for your location, units or materials before starting: a written scope is enough. If plans arrive with no scope, it asks one thing: all trades, or a specific scope? ## How long it takes | Job | Usually takes | | --- | --- | | Takeoff | 15 to 30 minutes | | Takeoff review | about 10 minutes | | Pricing | up to 15 minutes | | Pricing review | about 5 minutes | | Proposal | about 10 minutes | You don't need to keep the chat open. Your assistant checks every few minutes, gives you a one-line update, and picks up where it left off when you come back. A job that is still running after four hours is stopped; ask your assistant to pick it up again. ## What you get back - **A takeoff** you can review and edit row by row on the Field24 review page, with the assumptions the estimator made and the questions that most affect the price. - **A priced estimate**: unit and extended costs for every row, a bare cost and a suggested total with waste, contingency and margin, and the exclusions. - **A proposal**: a branded PDF your client can e-sign, as a download link. ## What you can ask next - "Change the drywall to 500 SF" or "remove the vanity": your assistant edits the row directly. - "The corridor walls are rated" or any answer to the estimator's questions: it's sent to the takeoff, which is re-measured. - "Re-price the framing" after a change: only those rows are priced again. - "Use my rate of $65 an hour for drywall labor": saved to your price book and used from then on. - "Stop the takeoff": the job stops; nothing is deleted. ## For client builders Every job runs through one tool, `field24_agent`: `create` starts a job and returns at once, `status` reads it, `follow-up` sends answers or corrections to the same job (or resumes it after a retryable failure), and `abort` stops it. The calls for a priced estimate, in order: `create_project`, then `add_project_files` or `get_project_upload_urls`, then `field24_agent` with type `takeoff`, `takeoff_review`, `pricing`, `pricing_review` and optionally `proposal`, checking `status` between each. Never wait inside a turn: check `status` again after a few minutes. Full details are in the [Tool reference](https://field24.ai/docs/mcp/tools.md). --- # Files URL: https://field24.ai/docs/mcp/files A takeoff measures what's in the project: plans, drawings, site photos or a written scope. Give your assistant the files and it puts them in the job's Field24 project, exactly as if you had uploaded them on the web. ## Three ways to add files 1. **Attach them in the chat.** In ChatGPT and other apps that pass attachments along, attach the plans to your message and your assistant adds them to the project (up to 20 files per message). 2. **Files on your computer.** Desktop apps that can run commands, such as the ChatGPT desktop app, Claude Code, Codex or Cursor, upload the files straight from your disk. Tell your assistant where they are. 3. **Upload them yourself.** If neither works, your assistant gives you a link to the project's upload page on Field24. Each file is reported on its own, so one bad file doesn't stop the rest. ## What a project accepts | | | | --- | --- | | Plans and documents | PDF, DOC, DOCX, XLS, XLSX, CSV, TXT, MD | | Photos and images | PNG, JPEG, WebP, HEIC, GIF, TIFF | | Archives | ZIP, for plan sets that arrive zipped | | Size | up to 300 MB per file | | Per project | up to 200 files | Executables, scripts, web pages, SVG and other archive formats are refused, as is any file that isn't really the type its name says. ## Seeing what's on file Ask "what files does this project have?" and your assistant lists them. Generated documents, such as proposals, show up in the project too. --- # Tool reference URL: https://field24.ai/docs/mcp/tools The 15 tools the Field24 MCP server gives your AI client. The client picks them; you just ask in plain words. This page is generated from the server itself, so it always matches what a client sees. | Tool | What it does | Effect | | --- | --- | --- | | [`get_profile`](#get-profile) | Get the signed-in Field24 user's profile: their stable account id, name, email and a short nickname to greet them by. | Reads only | | [`get_usage`](#get-usage) | Get the signed-in user's Field24 spend for the current billing month (`totalUsage`) against their included allowance (`includedUsageUsd`), plus their remaining prepaid credit (`currentBalanceUsd`) and whether extra usage beyond the allowance is enabled. | Reads only | | [`list_projects`](#list-projects) | List every project (job) on the signed-in contractor's Field24 account, with a count of the files in each. | Reads only | | [`create_project`](#create-project) | First step for any construction estimate, takeoff, bid or proposal request. | Can change data | | [`update_project`](#update-project) | Rename a project, change its client or status, or archive it (`archived: true`) and restore it (`archived: false`). | Can overwrite or delete data | | [`list_project_files`](#list-project-files) | List the files in one of the user's projects — drawings, site photos, receipts and generated documents — with their type and format. | Can change data | | [`add_project_files`](#add-project-files) | Add files the user attached in this conversation (plans, drawings, photos, documents) to one of their Field24 projects, so they sync into the user's Field24 workspace. | Can change data | | [`get_project_upload_urls`](#get-project-upload-urls) | Use this when the user's plans, drawings or photos are files on their computer (you have a local path, not a ChatGPT-hosted download URL). | Can change data | | [`get_project`](#get-project) | Get one of the user's projects with its files. | Can change data | | [`list_price_book`](#list-price-book) | List the signed-in contractor's price book — their own saved labor and material unit prices, newest first. | Reads only | | [`save_price_book_item`](#save-price-book-item) | Add a rate to the user's price book, or update the existing row when the same `type` + `item` is already saved (case-insensitive) — re-saving an item the user already has edits it rather than creating a duplicate. | Can overwrite or delete data | | [`update_price_book_item`](#update-price-book-item) | Edit one price book row by its `id` (from list_price_book). | Can overwrite or delete data | | [`delete_price_book_item`](#delete-price-book-item) | Permanently remove one price book row by its `id`. | Can overwrite or delete data | | [`field24_agent`](#field24-agent) | Runs Field24's estimating agents in the user's own Field24 workspace for a construction estimate, quantity takeoff, bid, pricing or proposal, from the project's plans, drawings, photos or written scope. | Can overwrite or delete data | | [`update_takeoff`](#update-takeoff) | Edit a Field24 takeoff record directly: change, add or remove rows by row id. | Can overwrite or delete data | ## Account ### get_profile Get the signed-in Field24 user's profile: their stable account id, name, email and a short nickname to greet them by. Reads only. #### Input and output **Input** No fields. **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string or null | yes | | | `email` | string or null | yes | | | `nickname` | string or null | yes | | ### get_usage Get the signed-in user's Field24 spend for the current billing month (`totalUsage`) against their included allowance (`includedUsageUsd`), plus their remaining prepaid credit (`currentBalanceUsd`) and whether extra usage beyond the allowance is enabled. All amounts are USD. Reads only. #### Input and output **Input** No fields. **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `totalUsage` | number | yes | | | `includedUsageUsd` | number | yes | | | `extraUsage` | boolean | yes | | | `currentBalanceUsd` | number | yes | | ## Projects and files ### list_projects List every project (job) on the signed-in contractor's Field24 account, with a count of the files in each. Archived projects are included and carry a non-null `archivedAt`. Use this to resolve a project the user names in conversation to its `projectId`. Reads only. #### Input and output **Input** No fields. **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projects` | array of any | yes | | ### create_project First step for any construction estimate, takeoff, bid or proposal request. Create a Field24 project for a job the user wants estimated. Call this first, once per job, before adding files or starting an estimate. Derive a short human name from the address or the work. Can change data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string, min length 1, max length 200 | yes | | | `client` | string, min length 1, max length 200 | no | | ```json { "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "client": { "type": "string", "minLength": 1, "maxLength": 200 } }, "required": [ "name" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | | `createdBy` | string | yes | | | `projectName` | string | yes | | | `client` | string or null | yes | | | `projectTag` | string | yes | | | `status` | any or null | yes | | | `archivedAt` | string or null | yes | | | `createdAt` | string | yes | | | `updatedAt` | string | yes | | | `uploadUrl` | string, format uri | yes | Field24 page where the signed-in user can add files to their projects by hand. | ### update_project Rename a project, change its client or status, or archive it (`archived: true`) and restore it (`archived: false`). Archiving is reversible and never deletes files or history. Can overwrite or delete data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | | `name` | string, min length 1, max length 200 | no | | | `client` | string or null | no | | | `status` | any | no | | | `archived` | boolean | no | | ```json { "type": "object", "properties": { "projectId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "client": { "anyOf": [ { "type": "string", "minLength": 1, "maxLength": 200 }, { "type": "null" } ] }, "status": { "allOf": [ { "$ref": "#/definitions/ProjectStatus" } ] }, "archived": { "type": "boolean" } }, "required": [ "projectId" ], "definitions": { "ProjectStatus": { "type": "string", "enum": [ "estimating", "proposal_sent", "won", "lost" ] } } } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | | `createdBy` | string | yes | | | `projectName` | string | yes | | | `client` | string or null | yes | | | `projectTag` | string | yes | | | `status` | any or null | yes | | | `archivedAt` | string or null | yes | | | `createdAt` | string | yes | | | `updatedAt` | string | yes | | ### list_project_files List the files in one of the user's projects — drawings, site photos, receipts and generated documents — with their type and format. Use it to tell the user what a job already has on file. Returns metadata only, never file contents or download links. Can change data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | ```json { "type": "object", "properties": { "projectId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "required": [ "projectId" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `files` | array of any | yes | | ### add_project_files Add files the user attached in this conversation (plans, drawings, photos, documents) to one of their Field24 projects, so they sync into the user's Field24 workspace. Use this to hand the user's attached plans, drawings and photos to Field24 instead of analysing them yourself. Pass every attached file in `files` — at most 20 per call. Each file is reported separately; a file that fails does not stop the others. Use this only when the file is attached in the chat and you were given a download URL for it. If you only have a local file path, use get_project_upload_urls instead. Only PDFs, images (PNG, JPEG, WebP, HEIC, GIF, TIFF), office documents (DOC, DOCX, XLS, XLSX, CSV, TXT, MD) and ZIP archives are accepted. Can change data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | | `files` | array of object | no | | | `files[].download_url` | string, min length 1, max length 4096 | yes | | | `files[].file_id` | string, min length 1, max length 500 | yes | | | `files[].mime_type` | string, max length 200 | no | | | `files[].file_name` | string, max length 500 | no | | ```json { "type": "object", "properties": { "projectId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "files": { "type": "array", "items": { "type": "object", "properties": { "download_url": { "type": "string", "minLength": 1, "maxLength": 4096 }, "file_id": { "type": "string", "minLength": 1, "maxLength": 500 }, "mime_type": { "type": "string", "maxLength": 200 }, "file_name": { "type": "string", "maxLength": 500 } }, "required": [ "download_url", "file_id" ] } } }, "required": [ "projectId" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `files` | array of object | yes | | | `files[].ok` | true | yes | | | `files[].fileId` | string | no | | | `files[].name` | string | yes | | | `files[].bytes` | integer, min 0 | no | | | `files[].error` | string | no | | ### get_project_upload_urls Use this when the user's plans, drawings or photos are files on their computer (you have a local path, not a ChatGPT-hosted download URL). Give each file's name and exact size in bytes. Upload each file with an HTTP PUT to its upload_url using exactly the returned headers, e.g. `curl -sS -X PUT -H 'content-type: application/pdf' --data-binary @'/path/to/plans.pdf' ''`. Then call get_project: uploaded files appear on the project automatically. Only PDFs, images, office documents and ZIP archives up to 300 MB are accepted. Do not open the URL in a browser. Can change data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | | `files` | array of object, min items 1, max items 20 | yes | | | `files[].file_name` | string, min length 1, max length 500 | yes | | | `files[].size_bytes` | integer | yes | | | `files[].mime_type` | string, max length 200 | no | | ```json { "type": "object", "properties": { "projectId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "files": { "minItems": 1, "maxItems": 20, "type": "array", "items": { "type": "object", "properties": { "file_name": { "type": "string", "minLength": 1, "maxLength": 500 }, "size_bytes": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "mime_type": { "type": "string", "maxLength": 200 } }, "required": [ "file_name", "size_bytes" ] } } }, "required": [ "projectId", "files" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `uploads` | array of object | yes | | | `uploads[].ok` | true | yes | | | `uploads[].file_name` | string | yes | | | `uploads[].upload_url` | string | no | | | `uploads[].method` | "PUT" | no | | | `uploads[].headers` | object | no | | | `uploads[].headers.content-type` | string | yes | | | `uploads[].headers.content-length` | string | yes | | | `uploads[].expires_in_seconds` | integer | no | | | `uploads[].error` | string | no | | ### get_project Get one of the user's projects with its files. Files uploaded through get_project_upload_urls are recorded here automatically; any upload that was not a valid file is listed under `pending` with a note. Can change data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, format uuid | yes | | ```json { "type": "object", "properties": { "projectId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "required": [ "projectId" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string | yes | | | `tag` | string | yes | | | `name` | string | yes | | | `client` | string or null | yes | | | `uploadUrl` | string | yes | | | `files` | array of object | yes | | | `files[].fileId` | string | yes | | | `files[].name` | string | yes | | | `files[].bytes` | integer or null | yes | | | `files[].type` | string | yes | | | `pending` | array of object | no | | | `pending[].name` | string | yes | | | `pending[].note` | string | yes | | ## Price book ### list_price_book List the signed-in contractor's price book — their own saved labor and material unit prices, newest first. Call this BEFORE quoting any number, so the estimate uses the user's real rates instead of generic averages. Use `q` to search item names and notes, and `type` to fetch only labor or only material rows. Reads only. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `q` | string, min length 1, max length 200 | no | | | `type` | "material" or "labor" | no | Which bucket the price falls in. "labor" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and "labour" folds into "labor". | | `activeOnly` | boolean | no | | | `limit` | integer, min 1, max 1000 | no | | ```json { "type": "object", "properties": { "q": { "type": "string", "minLength": 1, "maxLength": 200 }, "type": { "type": "string", "enum": [ "material", "labor" ], "description": "Which bucket the price falls in. \"labor\" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and \"labour\" folds into \"labor\"." }, "activeOnly": { "type": "boolean" }, "limit": { "type": "integer", "minimum": 1, "maximum": 1000 } } } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | array of any | yes | | ### save_price_book_item Add a rate to the user's price book, or update the existing row when the same `type` + `item` is already saved (case-insensitive) — re-saving an item the user already has edits it rather than creating a duplicate. `type` is "material" or "labor". `unit` is free text and is stored uppercased (e.g. SQFT, LF, HR, EA). `created` in the result says whether a new row was made. Always confirm the number with the user before saving it. Can overwrite or delete data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | "material" or "labor" | yes | Which bucket the price falls in. "labor" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and "labour" folds into "labor". | | `item` | string, min length 1, max length 200 | yes | | | `unit` | string, min length 1, max length 20 | yes | | | `unitPrice` | number, min 0, max 99999999.99 | yes | | | `currency` | "USD" or "CAD" | no | Currency the unit price is quoted in. Accepts any casing. | | `notes` | string or null | no | | | `markupPct` | number or null | no | | | `isActive` | boolean | no | | ```json { "type": "object", "properties": { "type": { "type": "string", "enum": [ "material", "labor" ], "description": "Which bucket the price falls in. \"labor\" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and \"labour\" folds into \"labor\"." }, "item": { "type": "string", "minLength": 1, "maxLength": 200 }, "unit": { "type": "string", "minLength": 1, "maxLength": 20 }, "unitPrice": { "type": "number", "minimum": 0, "maximum": 99999999.99 }, "currency": { "type": "string", "enum": [ "USD", "CAD" ], "description": "Currency the unit price is quoted in. Accepts any casing." }, "notes": { "anyOf": [ { "type": "string", "maxLength": 2000 }, { "type": "null" } ] }, "markupPct": { "anyOf": [ { "type": "number", "minimum": 0, "maximum": 999.99 }, { "type": "null" } ] }, "isActive": { "type": "boolean" } }, "required": [ "type", "item", "unit", "unitPrice" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string, format uuid | yes | | | `type` | "material" or "labor" | yes | Which bucket the price falls in. "labor" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and "labour" folds into "labor". | | `item` | string | yes | | | `unit` | string | yes | | | `unitPrice` | number | yes | | | `currency` | string | yes | | | `notes` | string or null | yes | | | `markupPct` | number or null | yes | | | `isActive` | boolean | yes | | | `createdAt` | string | yes | | | `updatedAt` | string | yes | | | `created` | boolean | yes | | ### update_price_book_item Edit one price book row by its `id` (from list_price_book). Send only the fields that change; at least one is required. Use this for a targeted edit such as bumping a unit price — use save_price_book_item when the user is adding a rate. Can overwrite or delete data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string, format uuid | yes | | | `type` | "material" or "labor" | no | Which bucket the price falls in. "labor" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and "labour" folds into "labor". | | `item` | string, min length 1, max length 200 | no | | | `unit` | string, min length 1, max length 20 | no | | | `unitPrice` | number, min 0, max 99999999.99 | no | | | `currency` | "USD" or "CAD" | no | Currency the unit price is quoted in. Accepts any casing. | | `notes` | string or null | no | | | `markupPct` | number or null | no | | | `isActive` | boolean | no | | ```json { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "type": { "type": "string", "enum": [ "material", "labor" ], "description": "Which bucket the price falls in. \"labor\" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and \"labour\" folds into \"labor\"." }, "item": { "type": "string", "minLength": 1, "maxLength": 200 }, "unit": { "type": "string", "minLength": 1, "maxLength": 20 }, "unitPrice": { "type": "number", "minimum": 0, "maximum": 99999999.99 }, "currency": { "type": "string", "enum": [ "USD", "CAD" ], "description": "Currency the unit price is quoted in. Accepts any casing." }, "notes": { "anyOf": [ { "type": "string", "maxLength": 2000 }, { "type": "null" } ] }, "markupPct": { "anyOf": [ { "type": "number", "minimum": 0, "maximum": 999.99 }, { "type": "null" } ] }, "isActive": { "type": "boolean" } }, "required": [ "id" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string, format uuid | yes | | | `type` | "material" or "labor" | yes | Which bucket the price falls in. "labor" is costed from an hourly rate; everything else is priced as a material. Accepts any casing, and "labour" folds into "labor". | | `item` | string | yes | | | `unit` | string | yes | | | `unitPrice` | number | yes | | | `currency` | string | yes | | | `notes` | string or null | yes | | | `markupPct` | number or null | yes | | | `isActive` | boolean | yes | | | `createdAt` | string | yes | | | `updatedAt` | string | yes | | ### delete_price_book_item Permanently remove one price book row by its `id`. Destructive — always confirm with the user first, and prefer setting `isActive: false` with update_price_book_item when they only want to stop using a rate. Can overwrite or delete data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string, format uuid | yes | | ```json { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "required": [ "id" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `deleted` | true | yes | | ## Estimating agents ### field24_agent Runs Field24's estimating agents in the user's own Field24 workspace for a construction estimate, quantity takeoff, bid, pricing or proposal, from the project's plans, drawings, photos or written scope. type: takeoff (scope of work and measured quantities from the project's plans, photos or written scope, 15 to 30 minutes), takeoff_review (Field24's reviewer checks a finished takeoff against the plans and returns findings, about 10 minutes), pricing (prices a takeoff from the user's rates and local store prices, up to 15 minutes), pricing_review (Field24's reviewer checks the priced takeoff and returns findings, about 5 minutes), proposal (a branded PDF, optional: you may write the proposal yourself from the priced takeoff instead). action: create starts the agent and returns a sessionId at once; status reads it; follow-up sends the user's answers or corrections to the same agent; abort stops it. Never wait in a turn: after create or follow-up, tell the user and call status when they return or ask. When a takeoff completes, start a takeoff_review instead of reviewing it yourself; when pricing completes, start a pricing_review. Show the user every assumption, question and finding from the replies. Apply accepted findings and corrections with action follow-up on the takeoff or pricing session, never by re-estimating yourself. Never present a quantity or price Field24 did not return. Can overwrite or delete data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | "takeoff" or "takeoff_review" or "pricing" or "pricing_review" or "proposal" | yes | Which agent: takeoff, takeoff_review, pricing, pricing_review or proposal. | | `action` | "create" or "status" or "follow-up" or "abort" | yes | create starts the agent; status reads it; follow-up sends the user's answers or corrections; abort stops it. | | `projectId` | string, format uuid | no | type takeoff + action create only (required there): the project whose plans, photos or written scope to take off. | | `scope` | string, min length 3, max length 2000 | no | type takeoff + action create only (required there): the scope of work the user locked, verbatim. | | `takeoffId` | string, format uuid | no | types takeoff_review, pricing, pricing_review and proposal + action create only (required there): the takeoff to review, price or build the proposal from. | | `rowIds` | array of string, min items 1 | no | type pricing + action create only (optional): re-price only these rows, e.g. unpricedRowIds. Omit to price the whole takeoff. | | `template` | string, min length 1, max length 100 | no | type proposal + action create only (optional): a proposal template tag the user saved in Field24. | | `sessionId` | string, min length 1, max length 256 | no | actions status, follow-up and abort (required there): the sessionId that create returned for this same type. | | `message` | string, min length 1, max length 4000 | no | action follow-up only (required there): the user's answers or corrections, in their words. | ```json { "type": "object", "properties": { "type": { "type": "string", "enum": [ "takeoff", "takeoff_review", "pricing", "pricing_review", "proposal" ], "description": "Which agent: takeoff, takeoff_review, pricing, pricing_review or proposal." }, "action": { "type": "string", "enum": [ "create", "status", "follow-up", "abort" ], "description": "create starts the agent; status reads it; follow-up sends the user's answers or corrections; abort stops it." }, "projectId": { "description": "type takeoff + action create only (required there): the project whose plans, photos or written scope to take off.", "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "scope": { "description": "type takeoff + action create only (required there): the scope of work the user locked, verbatim.", "type": "string", "minLength": 3, "maxLength": 2000 }, "takeoffId": { "description": "types takeoff_review, pricing, pricing_review and proposal + action create only (required there): the takeoff to review, price or build the proposal from.", "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "rowIds": { "description": "type pricing + action create only (optional): re-price only these rows, e.g. unpricedRowIds. Omit to price the whole takeoff.", "minItems": 1, "type": "array", "items": { "type": "string", "pattern": "^t\\d+$" } }, "template": { "description": "type proposal + action create only (optional): a proposal template tag the user saved in Field24.", "type": "string", "minLength": 1, "maxLength": 100 }, "sessionId": { "description": "actions status, follow-up and abort (required there): the sessionId that create returned for this same type.", "type": "string", "minLength": 1, "maxLength": 256 }, "message": { "description": "action follow-up only (required there): the user's answers or corrections, in their words.", "type": "string", "minLength": 1, "maxLength": 4000 } }, "required": [ "type", "action" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `sessionId` | string | yes | The agent session. Keep it: status, follow-up and abort take it. | | `status` | "running" or "completed" or "failed" or "aborted" | yes | running: check again after pollAfterSeconds. completed: the result fields for the type are present. failed: see error. aborted: stopped by abort. | | `expectedMinutes` | integer | no | create and follow-up only: how long the run usually takes. | | `pollAfterSeconds` | integer | no | create, follow-up and status running: wait at least this long before checking status. | | `startedAt` | string | no | status running only: when the session started (ISO 8601). | | `reply` | string | no | status completed only: the agent's final message, with its assumptions and questions — for takeoff_review and pricing_review, the numbered findings. Show them to the user. | | `takeoffId` | string | no | status completed only: the takeoff the agent wrote or worked on. | | `reviewUrl` | string | no | takeoff, takeoff_review, pricing and pricing_review completed only: the Field24 page where the user can review and edit rows by hand. | | `summary` | object | no | takeoff completed only: leaf row count, division count and rows with no quantity. | | `summary.rows` | integer | yes | | | `summary.divisions` | integer | yes | | | `summary.rowsWithoutQty` | integer | yes | | | `items` | array of object | no | takeoff and pricing completed only: the takeoff as it stands now; pricing adds the prices. | | `items[].id` | string | no | Stable row id (t1, t2, …) — use it in rowIds. | | `items[].div` | string | yes | CSI division. | | `items[].item` | string | yes | | | `items[].description` | string | yes | | | `items[].qty` | number or null | yes | Measured quantity; null = the agent could not settle one. | | `items[].unit` | string | yes | | | `items[].confidence` | "HIGH" or "MEDIUM" or "LOW" | yes | | | `items[].sources` | array of object | yes | | | `items[].sources[].document` | string | yes | | | `items[].sources[].pages` | array of integer | yes | | | `items[].unitCost` | number | no | pricing only. | | `items[].extCost` | number | no | pricing only: bare cost, quantity × unit cost, no waste or markup. | | `items[].suggestedCost` | number | no | pricing only: client-facing price for the row, waste, contingency and margin included. | | `items[].costType` | "material" or "labor" | no | | | `items[].wastePct` | number | no | | | `items[].children` | array of object | no | An assembly's component rows; the parent carries no money of its own. | | `items[].children[].id` | string | no | Stable row id (t1, t2, …) — use it in rowIds. | | `items[].children[].div` | string | yes | CSI division. | | `items[].children[].item` | string | yes | | | `items[].children[].description` | string | yes | | | `items[].children[].qty` | number or null | yes | Measured quantity; null = the agent could not settle one. | | `items[].children[].unit` | string | yes | | | `items[].children[].confidence` | "HIGH" or "MEDIUM" or "LOW" | yes | | | `items[].children[].sources` | array of object | yes | | | `items[].children[].unitCost` | number | no | pricing only. | | `items[].children[].extCost` | number | no | pricing only: bare cost, quantity × unit cost, no waste or markup. | | `items[].children[].suggestedCost` | number | no | pricing only: client-facing price for the row, waste, contingency and margin included. | | `items[].children[].costType` | "material" or "labor" | no | | | `items[].children[].wastePct` | number | no | | | `cost` | number | no | pricing completed only: bare cost, Σ leaf quantity × unit cost. | | `suggestedTotal` | number | no | pricing completed only: the client-facing total — waste, contingency and margin included. | | `currency` | "USD" or "CAD" or null | no | pricing completed only. | | `storeLocation` | string or null | no | pricing completed only: the store material prices came from. | | `opPct` | number or null | no | pricing completed only: overhead and profit percent applied. | | `contingencyPct` | number or null | no | pricing completed only. | | `exclusions` | array of any | no | pricing completed only. | | `unpricedRowIds` | array of string | no | pricing completed only: rows with a quantity and no price. Never present a total while this is not empty. | | `files` | array of object | no | proposal completed only: the proposal PDFs. The links expire; call status again for fresh ones. | | `files[].name` | string | yes | | | `files[].url` | string or null | yes | Short-lived download link; null when it could not be made. | | `files[].note` | string | no | | | `error` | object | no | status failed only: what went wrong; when retryable, offer to start it again. | | `error.code` | string, min length 1 | yes | | | `error.message` | string | yes | | | `error.retryable` | boolean | yes | | ## Takeoff edits ### update_takeoff Edit a Field24 takeoff record directly: change, add or remove rows by row id. Use this ONLY for a change the user states explicitly with its value (for example 'change the drywall to 500 SF' or 'remove the vanity'). Anything that needs measuring, re-reading the plans, or judgement — including every reviewer finding — goes to the estimator with field24_agent action follow-up instead. Read the current rows first (field24_agent takeoff status) and copy row ids exactly. After a quantity change on a priced row, re-run pricing for that row. Can overwrite or delete data. #### Input and output **Input** | Field | Type | Required | Description | | --- | --- | --- | --- | | `takeoffId` | string, format uuid | yes | The takeoff to edit. | | `items` | array of object, min items 1, max items 100 | no | Rows to change (by id) or add (no id). Send only the fields the user changed on an existing row. | | `items[].id` | string, pattern ^t\\d+$ | no | The row's id from field24_agent takeoff status (t1, t2, …), copied exactly. Required to change a row; omit ONLY to add a new row. | | `items[].div` | string, min length 1, max length 20 | no | CSI division, e.g. 09. Required for a new row. | | `items[].item` | string, min length 1, max length 200 | no | Row name. Required for a new row. | | `items[].description` | string, max length 2000 | no | Required for a new row. | | `items[].qty` | number, min 0 | no | The quantity the user stated. Required for a new row. | | `items[].unit` | string, min length 1, max length 20 | no | SF, LF, EA, … Required for a new row. | | `items[].unitCost` | number, min 0 | no | Optional: the unit cost the user stated. | | `items[].children` | array of object, min items 1 | no | Only when editing an assembly's child SET: the full list of its children (a child left out is removed). Every existing child carries its id. To change one child, send that child by its own id instead. | | `items[].children[].id` | string, pattern ^t\\d+$ | no | The row's id from field24_agent takeoff status (t1, t2, …), copied exactly. Required to change a row; omit ONLY to add a new row. | | `items[].children[].div` | string, min length 1, max length 20 | no | CSI division, e.g. 09. Required for a new row. | | `items[].children[].item` | string, min length 1, max length 200 | no | Row name. Required for a new row. | | `items[].children[].description` | string, max length 2000 | no | Required for a new row. | | `items[].children[].qty` | number, min 0 | no | The quantity the user stated. Required for a new row. | | `items[].children[].unit` | string, min length 1, max length 20 | no | SF, LF, EA, … Required for a new row. | | `items[].children[].unitCost` | number, min 0 | no | Optional: the unit cost the user stated. | | `remove` | array of object, min items 1, max items 100 | no | Rows to remove, by id. | | `remove[].id` | string, pattern ^t\\d+$ | yes | | ```json { "type": "object", "properties": { "takeoffId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "The takeoff to edit." }, "items": { "description": "Rows to change (by id) or add (no id). Send only the fields the user changed on an existing row.", "minItems": 1, "maxItems": 100, "type": "array", "items": { "type": "object", "properties": { "id": { "description": "The row's id from field24_agent takeoff status (t1, t2, …), copied exactly. Required to change a row; omit ONLY to add a new row.", "type": "string", "pattern": "^t\\d+$" }, "div": { "description": "CSI division, e.g. 09. Required for a new row.", "type": "string", "minLength": 1, "maxLength": 20 }, "item": { "description": "Row name. Required for a new row.", "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "description": "Required for a new row.", "type": "string", "maxLength": 2000 }, "qty": { "description": "The quantity the user stated. Required for a new row.", "type": "number", "minimum": 0 }, "unit": { "description": "SF, LF, EA, … Required for a new row.", "type": "string", "minLength": 1, "maxLength": 20 }, "unitCost": { "description": "Optional: the unit cost the user stated.", "type": "number", "minimum": 0 }, "children": { "description": "Only when editing an assembly's child SET: the full list of its children (a child left out is removed). Every existing child carries its id. To change one child, send that child by its own id instead.", "minItems": 1, "type": "array", "items": { "type": "object", "properties": { "id": { "description": "The row's id from field24_agent takeoff status (t1, t2, …), copied exactly. Required to change a row; omit ONLY to add a new row.", "type": "string", "pattern": "^t\\d+$" }, "div": { "description": "CSI division, e.g. 09. Required for a new row.", "type": "string", "minLength": 1, "maxLength": 20 }, "item": { "description": "Row name. Required for a new row.", "type": "string", "minLength": 1, "maxLength": 200 }, "description": { "description": "Required for a new row.", "type": "string", "maxLength": 2000 }, "qty": { "description": "The quantity the user stated. Required for a new row.", "type": "number", "minimum": 0 }, "unit": { "description": "SF, LF, EA, … Required for a new row.", "type": "string", "minLength": 1, "maxLength": 20 }, "unitCost": { "description": "Optional: the unit cost the user stated.", "type": "number", "minimum": 0 } } } } } } }, "remove": { "description": "Rows to remove, by id.", "minItems": 1, "maxItems": 100, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^t\\d+$" } }, "required": [ "id" ] } } }, "required": [ "takeoffId" ] } ``` **Output** | Field | Type | Required | Description | | --- | --- | --- | --- | | `takeoffId` | string | yes | | | `reviewUrl` | string | yes | The Field24 page where the user can see and edit the takeoff. | | `changed` | array of string | yes | Row ids whose fields changed. | | `added` | array of string | yes | Ids of the rows this edit added. | | `removed` | array of string | yes | Ids of the rows this edit removed. | | `summary` | object | yes | The takeoff after the edit: leaf rows, divisions and rows with no quantity. | | `summary.rows` | integer | yes | | | `summary.divisions` | integer | yes | | | `summary.rowsWithoutQty` | integer | yes | | --- # Errors and limits URL: https://field24.ai/docs/mcp/errors-and-limits When something goes wrong, Field24 tells your assistant what happened and what to do next, and your assistant passes it on in plain words. These are the ones you might see. ## Errors | What you see | What to do | | --- | --- | | Your free trial has ended, or your subscription lapsed (`subscription_required`) | Subscribe at the link your assistant gives you, then ask again. Projects, files and your price book keep working meanwhile. | | Your Field24 workspace is still being set up (`no_agent`) | Happens right after a new account signs in. Wait a minute and try again; if it keeps happening, open Field24 once at `https://field24.ai/app`. | | Field24 couldn't reach your workspace (`harness_unavailable`) | Nothing was saved. Try again in a minute. | | A takeoff, pricing or proposal run failed or timed out | Ask your assistant to pick it up again: it resumes the same run where it can, or starts a new one. | | Field24 needs a permission (`insufficient_scope`) | Reconnect Field24 in your assistant and approve it. | | A proposal or pricing review was asked for before pricing finished (`not_priced`) | Ask your assistant to price the takeoff first, or the rows it names. | | A file was refused | The message says why: not an accepted type, empty, over 300 MB, or the project is full. See [Files](https://field24.ai/docs/mcp/files.md). | | The takeoff changed while it was being edited (`takeoff_changed`) | Ask again; your assistant reads the latest rows and retries. | For anything else, ask your assistant to try again; if it keeps failing, use the [support form](https://field24.ai/support). ## Limits | | | | --- | --- | | Files | PDF, images, office documents and ZIP; up to 300 MB each, 20 per message, 200 per project | | Takeoff | 15 to 30 minutes | | Pricing | up to 15 minutes | | Reviews | 5 to 10 minutes each | | Longest run | 4 hours, then it is stopped | | Agent work | needs a Field24 free trial or subscription | | Sign-in | renewed automatically; every few days you sign in again | ## For client builders A tool error is a normal tool result with `isError: true`, a message written for the model to act on, and a machine-readable code in `_meta["field24/errorCode"]`. A failed job reads as `status: "failed"` with `error.code` (`agent_failed`, `timeout` or `no_takeoff`) and `error.retryable`. Requests without a valid token get `401` with a `WWW-Authenticate` header that starts sign-in, and very high request rates get `429`.