---
title: "Build a software factory"
description: "Build your own UI and workflows on Hoplite’s managed agent execution, workspaces, and PR operations."
canonical_url: "https://hoplite.sh/docs/factory"
markdown_url: "https://hoplite.sh/docs/factory.md"
---

# Build a software factory
URL: /docs/factory
LLM index: /llms.txt
Description: Build your own UI and workflows on Hoplite’s managed agent execution, workspaces, and PR operations.
Related: /docs/factory/events, /docs/factory/retries, /docs/factory/webhooks, /docs/api

# Build a software factory

Build your own cloud software factory on Hoplite. The public API gives your application repository-backed projects, persistent thread workspaces, agent execution, diffs, checkpoints, and PR operations. Your backend controls job admission and approval policy, and you choose the UI and workflow engine.

This guide takes a budgeted coding task from a GitHub-backed project to a draft pull request.

## Before running

Use Node 24. The example performs real work, consumes credits, and opens a draft PR, so choose a disposable repository and project for your first run.

1. Complete [cloud setup](/docs/quickstart#connect-a-repository): select a team workspace, authorize the GitHub App, enable billing, and create a project for the test repository.
2. Find the project's `id` with [List projects](/docs/api/listProjects). For this setup request, use a personal API key from **Settings → Workspace → API keys** in the same team workspace. Match the project by name and repository.
3. As an owner or admin, [create a service account](/docs/api/authentication#create-a-service-account). Enter that project ID in **Project IDs**, select the grants below, and save the one-time credential in your backend environment.

```text title="Service account permissions"
project:read
thread:create
thread:read
thread:update
modelKey:read
```

Keep the service credential in your backend environment as `HOPLITE_API_KEY`. [Authentication](/docs/api/authentication) covers expiry, rotation, and account administration. A service account does not inherit a user's personal subscription entitlements.

### Create projects from your factory

If your integration creates projects, give its service account `project:create` and `repo:read` in addition to the grants above, and leave **Project IDs** blank for organization-wide access. Use [List connected repositories](/docs/api/listGitHubRepositories) to find the repository ID after the owner has authorized the GitHub App.

Supply `HOPLITE_REPOSITORY_ID` instead of `HOPLITE_PROJECT_ID`. The example calls [Create project](/docs/api/createProject) using a stable retry key, then runs the same thread workflow.

## Run a factory job

[Download public-factory.mjs](/docs/factory-example.mjs), then configure the job:

```bash title="Run against your disposable project"
export HOPLITE_API_URL="https://api.hoplite.sh"
export HOPLITE_PROJECT_ID="your-project-id"
export HOPLITE_FACTORY_ID="factory-job-001"
export HOPLITE_PROMPT="Add a regression test for the login redirect"
export HOPLITE_SPEND_LIMIT_MICROS="1000000"
# HOPLITE_API_KEY is supplied by your backend environment.
node public-factory.mjs
```

The published hosted API is `https://api.hoplite.sh`; the disposable project provides your test boundary. Override the URL only if you have been given another Hoplite environment. Replace the project ID and task with your values. `1000000` microdollars is a $1 lifetime thread budget. Choose a budget sufficient for the task; a spend limit can pause execution before it reaches a PR.

Persist `HOPLITE_FACTORY_ID` with your job record. Reuse the same ID and inputs when retrying that job. The example derives stable write keys from this ID; changing it starts independent work.

## Adapt the API flow

The download implements this sequence. Keep the resource IDs and operation keys in your own job record when adapting it:

| Step | Operations | Keep for the next step |
| --- | --- | --- |
| Check access | [Capabilities](/docs/api/getFactoryCapabilities), [models](/docs/api/listModels), [project repositories](/docs/api/listProjectRepositories) | A model available to the service account and the target project ID |
| Start work | [Capture event head](/docs/api/getEventHead), then [create thread](/docs/api/createThread) with the prompt, model, and spend limit | The initial cursor, thread ID, run ID, and write key |
| Follow the run | [Replay events](/docs/api/listPublicEvents), [read thread](/docs/api/getThread), and [list runs](/docs/api/listThreadRuns) | The next cursor and current run status |
| Unblock or recover | [List approvals](/docs/api/listThreadApprovals), [resolve approval](/docs/api/resolveToolApproval), or [read failure](/docs/api/getRunFailure) | The decision or failure before resuming |
| Review and publish | [Read diff](/docs/api/getThreadDiff), [list checkpoints](/docs/api/listThreadCheckpoints), then [create draft PR](/docs/api/createPullRequest) | The PR URL and checkpoint IDs |

A successful thread-creation response accepts the job; it does not mean the run has completed. Wait for the run's terminal state before publishing its result.

## Follow execution

The example discovers capabilities and models, creates the thread, polls durable events and run state, then reads its diff and checkpoints. On success, it prints a JSON result containing:

| Field | Use in your application |
| --- | --- |
| `threadId`, `projectId` | Persist the Hoplite resources associated with your job |
| `pullRequest` | Display the resulting draft PR |
| `diff`, `checkpoints` | Offer code review and recovery controls |
| `cursor` | Continue reading events from the last observed position |
| `mergeResult` | Null unless explicit merge mode was enabled |

An error or client timeout does not cancel the cloud run. Read its current state and stop the active run explicitly if that is your policy. Do not create another thread to recover from an uncertain response; use [operation receipts](/docs/factory/retries).

## Handle approvals

The example exits when it encounters an actionable tool approval so your application can make a policy decision. Resolve it through [the approvals API](/docs/api/resolveToolApproval), then resume with the same factory ID and inputs.

`HOPLITE_APPROVE_TOOLS=true` makes the example approve actionable tool requests automatically. Enable that only when it matches your policy. Production integrations should evaluate the individual operation and input.

Draft creation is the default. `HOPLITE_MERGE=true` creates a ready PR and attempts a squash merge using the current expected head SHA. Your workflow remains responsible for deciding whether the change should merge.

## Attachments and previews

Set `HOPLITE_ATTACHMENT_PATH` to exercise the example's initial-attachment flow. A custom integration must upload to the presigned URL using the returned `upload.contentType`, without forwarding the Hoplite credential. Use the thread creation retry key as `initialClientOperationId` and pass `initialAttachmentTickets` when creating the thread. For an existing thread, confirm the upload ticket through its attachment endpoint.

To show a preview inside your own application, [start the preview](/docs/api/startThreadPreview), then call [Issue preview gateway access](/docs/api/getThreadPreviewAccess) with your dashboard's exact HTTPS `frameOrigin`. It returns the preview URL and a five-minute gateway credential. Your backend relays HTTP and WebSocket requests to that URL with the credential in `X-Hoplite-Preview-Token`, the same gateway model as [Platform embedded previews](/docs/platform/previews). Keep the credential on your server and request fresh access before it expires.

## Extend the workflow

- [Events and live UI](/docs/factory/events) — snapshots, SSE, reconnects, and streaming drafts.
- [Retries and operation receipts](/docs/factory/retries) — handle timeouts without duplicating work.
- [Signed webhooks](/docs/factory/webhooks) — receive state changes in your workflow engine.
- [API reference](/docs/api) — schemas for individual operations.

## Sitemap

See the full [sitemap](/docs/sitemap.md) for all pages.
Well-known sitemap: [/docs/.well-known/sitemap.md](/docs/.well-known/sitemap.md).
