Building with prompts
Drive the site agent from your code: write the prompt, follow the run, answer its questions, approve its plans and publish.
On this page
The site agent is the same one the editor's chat talks to. Over the API you talk to it with send_message and listen with get_job, and everything else in this guide is a variation on that loop:
- Send a prompt.
- Follow the run until it ends.
- If the agent asked a question or proposed a plan, reply with another message.
- Publish when you are happy with the result.
Before you begin
The token needs chat:write to send and chat:read to follow, plus publish:write to publish and sites:create to start a new site. Every prompt spends AI credits from the workspace that owns the site, and a call made with an empty balance is refused with a 402. See Credits.
Start a site from a prompt
create_site creates a site and starts its first build in one call. Describe the whole site the way you would brief a developer:
curl -X POST https://api.modulify.ai/v1/create_site \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Idempotency-Key: 1b6f0a52-7c3e-4c86-9d1e-5f2a8b7c4d90" \
-d '{"workspaceId":"WORKSPACE_ID","prompt":"A site for a two-chair barbershop in Lisbon. Home page with opening hours and a booking button, a prices page, and a contact page with a map. Warm, simple, mobile first."}'The answer carries the new site and a jobId to follow. To rebuild an existing website instead of starting from nothing, add importSource with the address of its page in SourceUrl, and say in the prompt what should change. A refusal for the plan's site limit or an empty credit balance comes back as a 403 with data.reason.
Write a good prompt
The prompt is the only steering the agent gets, so it pays to write it well. The same advice as for the editor's chat applies. See Writing prompts.
- Say who it is for and what it must do. "A booking page for a two-chair barbershop, where the main job is picking a time" describes one site. "Modern and clean" describes every site.
- Give concrete constraints. Numbers of items, what goes above what, what must stay, what must never happen.
- One clear request per message. Five unrelated asks in one paragraph are harder to get right than five messages, and you can send the next one while the first runs. It waits in the queue.
- Follow-ups inherit the conversation. "Now make it responsive" is a complete request. You never restate the whole site.
- Never paste a secret. Ask for the integration, and the agent creates the variable with a placeholder for you to fill in with
set_secret.
A prompt is refused over 500,000 characters.
Send a message
curl -X POST https://api.modulify.ai/v1/send_message \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Idempotency-Key: 9c2d4e6f-1a3b-4c5d-8e7f-0a1b2c3d4e5f" \
-d '{"projectId":"SITE_ID","prompt":"Add a prices page with three tiers, the middle one highlighted.","mode":"build"}'| Answer | What it means |
|---|---|
queued: false with a jobId |
The run started. Follow it with get_job |
queued: true with a queueItemId |
The agent is busy, so the message is waiting in line at queuePosition. It runs on its own, unless queueResumable is true |
Both are successes. Do not send a queued message again.
Build or plan
mode picks how the agent handles the message:
mode |
What happens |
|---|---|
build |
The agent carries the request out. This is the default |
plan |
The agent investigates, writes a plan of what it would change, and stops without touching a file |
Use plan for anything large or open to more than one reading. When the plan is ready, the run ends with a plan_approval_requested event in get_job, and the plan itself is in the run's text events.
- To approve it, send a message in
buildmode saying so, for exampleApproved. Build the plan.That is what the editor's Approve and build button sends. - To change it, reply with what you want different, in
planmode, and a revised plan comes back.
A reply to a waiting plan never queues. It runs at once, whatever else is waiting. See Plan and Build modes.
Follow the run
get_job reports the run's state and the events it produced. Start with cursor 0 and send back the cursor from each answer, so every call returns only what is new:
curl -X POST https://api.modulify.ai/v1/get_job \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"projectId":"SITE_ID","jobId":"JOB_ID","cursor":0}'Events you will see:
event |
What it carries |
|---|---|
text |
What the agent writes back, in pieces as it goes |
tool_call, tool_result |
The steps it takes, such as reading and editing files |
file_changed, file_deleted |
The files it changed |
todos |
Its task list for larger work |
plan_approval_requested |
A plan is waiting for your reply |
credits_used |
What the run cost |
error |
What went wrong, on a failed run |
Call it every few seconds while state is running. The run is over at succeeded, failed or cancelled, and creditsCharged holds what it cost. Long-running work explains every state, including runs that stop part way through.
Answer the agent's questions
When a choice would change the result, the agent stops and asks rather than guessing. The question arrives as a tool_call event whose data.name is ask_user, with the question in data.input.question and up to four suggested answers in data.input.options. The run then ends and waits for you.
Answer with send_message, in your own words or with one of the options:
curl -X POST https://api.modulify.ai/v1/send_message \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"projectId":"SITE_ID","prompt":"Use the warm palette from the home page."}'An answer never queues. It runs at once, and anything already waiting in the queue waits until you have answered.
Manage the queue
Messages sent while the agent is busy wait in the site's queue, the same queue the people in the workspace see in the editor. A site holds at most 20 waiting messages and 8 from any one person. Going over either is refused with a 429.
| Method | What it does |
|---|---|
list_queued_messages |
The waiting messages, oldest first, each with the QueueVersion that editing needs |
edit_queued_message |
Rewrites a message you queued, before it runs. Send the QueueVersion you just read as expectedVersion |
reorder_queued_message |
Moves a message earlier or later |
remove_queued_message |
Takes a message out of the queue. Needs sites:delete |
resume_queue |
Restarts a queue that stopped after a failed or stopped run |
After a run is stopped or fails, the queue stops on purpose until someone has looked at what went wrong, and a message sent then comes back with queueResumable: true. Read the failure with get_job, tidy the queue, then call resume_queue. See Message queue.
Stop a run
cancel_job stops the running generation. Whatever the agent already wrote stays written. Add clearSession: true to also start the agent's next conversation fresh.
Read the conversation
list_messages returns the chat history of a site, the same transcript the editor shows, newest page first. Read it before sending a new message from a script that did not start the conversation, so you do not repeat a request. See Pagination for reading older pages.
Images and files
send_message carries text only. To put an image of your own on the site, upload it as a public file with upload_storage_file, which answers with its permanent address, and name that address in the prompt.
Publish
When the result is right, publish_site puts it live, and get_publish_status reports when it is done:
curl -X POST https://api.modulify.ai/v1/publish_site \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"projectId":"SITE_ID"}'See Long-running work.
The whole loop
const API = 'https://api.modulify.ai/v1'
const HEADERS = { Authorization: `Bearer ${process.env.MODULIFY_TOKEN}` }
const Call = async (method, body = {}) => {
const response = await fetch(`${API}/${method}`, { method: 'POST', headers: HEADERS, body: JSON.stringify(body) })
const result = await response.json()
if (!result.success) throw new Error(`${method}: ${result.code} ${result.message}`)
return result.data
}
const Sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const RunPrompt = async (projectId, prompt, mode = 'build') => {
const sent = await Call('send_message', { projectId, prompt, mode })
if (sent.queued) return { queued: true, position: sent.queuePosition }
let cursor = 0
let question = null
let planReady = false
while (true) {
const job = await Call('get_job', { projectId, jobId: sent.jobId, cursor })
if (job.reset) {
question = null
planReady = false
}
for (const item of job.events) {
if (item.event === 'tool_call' && item.data.name === 'ask_user') question = item.data.input
if (item.event === 'plan_approval_requested') planReady = true
}
cursor = job.cursor
if (!['running', 'stalled'].includes(job.state)) return { state: job.state, question, planReady, error: job.error }
await Sleep(4000)
}
}
const projectId = 'SITE_ID'
const outcome = await RunPrompt(projectId, 'Add a prices page with three tiers, the middle one highlighted.', 'plan')
if (outcome.planReady) await RunPrompt(projectId, 'Approved. Build the plan.', 'build')
await Call('publish_site', { projectId })Next
- Long-running work for every state a run can end in.
- Idempotency and retries so a dropped connection never sends a prompt twice.