---
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 answers structured questions, 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). Under **Access**, choose **Selected** and add that project, set **Projects** to **Read**, **Threads** to **Write**, and **Model keys** to **Read**, and leave every other scope on **None**. Save the one-time credential in your backend environment. The example uses these permissions:

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

**Write** on Threads also grants `thread:stop` and `thread:retry`.

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. Its runs try [subscriptions moved to the organization](/docs/agent/models#organization-subscriptions) (and, for ChatGPT models, the organization's default ChatGPT member's subscription if an admin set one) before workspace keys, gateways, and credits.

A service account's commits and pull requests are authored by Hoplite. To have them authored by a member instead, pass that member's user ID as `onBehalfOfUserId` when you [create a thread](/docs/api/createThread); the thread's commits and pull requests are then authored as that member, whoever contributes to it later. It requires the workspace setting above and a service account with `actAsMember: true`. That grant is off by default, and only an organization owner can set it when creating or updating the account. Turning it on, re-granting it as a different owner, or renewing a lapsed grant revokes every existing credential of the account and returns one new credential in that response, to the owner only, so no key issued earlier can use the grant. It stops working, and is cleared, once that owner stops being an owner, and an edit or credential rotation by anyone else clears it. The member must belong to the organization with a role that can create threads, no higher than the granting owner's, and must not be banned. Grant changes and every use are recorded in the organization's settings audit log. Hoplite checks the member and the grant again each time it uses the member's GitHub account. It changes authorship only: billing, usage, and the message author stay with the service account. If the member has no usable linked GitHub account, Hoplite authors the work as usual.

### 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: set **Projects** to **Write** and **Repositories** to **Read**, and leave the project scope on **All projects** 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 | Read active questions with [List active structured questions](/docs/api/listThreadQuestions), answer with `POST /api/threads/{id}/questions/{questionId}`, or [read failure](/docs/api/getRunFailure) | The answer 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).

## Answer questions

The example discovers active questions through `GET /api/threads/{id}/questions`, which returns each question's ID, run ID, and structured request without exposing tool-call details. Provide an `onQuestion` callback to your workflow and return an answer map keyed by each question's text; without a callback, the example stops and identifies the question that needs an answer.

Answers are submitted to `POST /api/threads/{id}/questions/{questionId}` with an idempotency key derived from the factory ID and question ID. The example never approves or denies tool actions.

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.
