# Building with prompts

Source: https://modulify.ai/docs/api/building-with-prompts

Drive the site agent from your code: write the prompt, follow the run, answer its questions, approve its plans and publish.

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:

1. Send a prompt.
2. Follow the run until it ends.
3. If the agent asked a question or proposed a plan, reply with another message.
4. 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](https://modulify.ai/docs/plans/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:

```bash
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](https://modulify.ai/docs/build/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

```bash
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 `build` mode saying so, for example `Approved. Build the plan.` That is what the editor's **Approve and build** button sends.
- **To change it,** reply with what you want different, in `plan` mode, 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](https://modulify.ai/docs/build/plan-and-build).

## 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:

```bash
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](https://modulify.ai/docs/api/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:

```bash
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](https://modulify.ai/docs/build/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](https://modulify.ai/docs/api/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:

```bash
curl -X POST https://api.modulify.ai/v1/publish_site \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"projectId":"SITE_ID"}'
```

See [Long-running work](https://modulify.ai/docs/api/long-running-work#publishes).

## The whole loop

```javascript
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](https://modulify.ai/docs/api/long-running-work) for every state a run can end in.
- [Idempotency and retries](https://modulify.ai/docs/api/idempotency) so a dropped connection never sends a prompt twice.