Docs menu

Configure Previews

Previews

A step can show its output to a reviewer beside the run. There are two kinds. An app preview serves the running app from the sandbox of the run. A plan page is one file that the agent writes and pushes, for a step that makes a document and not an app.

Two kinds of preview

A step declares a preview: block. The block shows one kind of output, never two.

KindFieldWhat the reviewer seesRuntime
App previewservicesThe running app, served from the E2B sandbox of the runE2B only
Plan pagepageOne page that the agent of the step writes and pushes: HTML, Markdown or plain textAny
Outside URLneitherA URL that a producer outside Factorial publishesAny

For an app preview, the agent writes the app. The preview starts by itself when a reviewer opens it on the latest attempt, during the session of the agent or after it. The runtime starts the services. Factorial publishes their URLs after the readiness checks pass. The agent needs no deployment command and no instruction to publish a URL.

Before you start, connect the repository (Connect a repository). You need access to the project settings in the dashboard and to the repository. Read Previews and served pages before you enable either kind. It says why a preview sandbox is public while it lives. It also says why you must never type a credential into a preview.

The last section, When a preview fails, covers both kinds.

Preview the running app

Step 1. Choose what to serve

  1. Read the package manifests, lockfiles, start scripts and environment examples of the app.
  2. Choose a frontend-only preview or a preview with an API and a database.
  3. List the install commands, workspace builds, migrations and seed data that the app needs.
  4. Test the commands in a clean development environment.

For a frontend-only preview, find which features need an API. A page that renders does not prove that those features work. For a full app, arrange a disposable database and set the API connection yourself. Factorial does not provision these from preview:. See Full-stack previews.

Check the browser-facing API URLs, CORS and HTTPS. Browser code cannot reach an API in the sandbox through localhost. Use an endpoint that the browser can reach, or an app proxy. Factorial does not put service URLs into environment values.

Step 2. Select an E2B runtime

  1. Open Runtimes in the dashboard.
  2. Choose a runtime with E2B isolation. Its family must match the agent manifests. If none exists, an operator registers one (Register a runtime). The operator also chooses its template.
  3. Add its slug to the top-level runtime field of the process.
  4. Open the project configuration, then Permits, then E2B runtimes.
  5. Enable the runtime for the project.
# Use the slug from the Runtimes page of your deployment.
runtime: team-e2b

The process selects a runtime and never creates one. A host runtime cannot keep a sandbox for a preview. See Select a runtime for a process and Allowlists.

Step 3. Enable downloads before the build

  1. Open the project configuration, then Permits, then Egress hosts.
  2. Add the hosts that installation and startup need. Write hostnames only, with no scheme and no path.
  3. Save the settings before the build sandbox starts.

For npm, pnpm or npx, add registry.npmjs.org. Add more hosts if a dependency downloads binaries or the app calls an outside service.

The sandbox keeps the egress list that it had when the build run started. A restart of the preview uses the same list. After you change the list, use a new build run.

Step 4. Configure the preview

Edit .factorial/processes/process-<name>.yaml on the factorial branch of the repository. Add preview: to the agent step whose output you review, next to workspace and onSuccess. This block assumes an npm app whose dev script accepts --host and --port. Adapt the commands to the app. The block is not a complete process file.

  preview:
    setup:
      - "npm ci"
    services:
      - name: web
        command: "npm run dev -- --host 0.0.0.0 --port $PORT"
        port: 3000
        ready:
          path: "/"
          timeout: 120s
    stopAfter: 30m
  1. Use the package manager and the lockfile of the repository in setup.
  2. Add builds for shared workspace packages before the service starts.
  3. Give each service a unique name and a unique port.
  4. Keep each service command in the foreground. Bind web servers to 0.0.0.0.
  5. Set the host checks of the app to accept its E2B hostname, if it has them.
  6. Choose a readiness path that answers without authentication.
  7. Put the environment values that the app needs under preview.env. Setup does not receive them.

Example, after you store a project secret named PREVIEW_DATABASE_URL:

    env:
      DATABASE_URL: "{secrets.PREVIEW_DATABASE_URL}"

Use preview-only data and credentials. The sandbox is public while the preview runs. The runtime removes the credentials of the build agent before it parks the sandbox, so do not depend on them.

Fields of the preview block

FieldDefaultRule
enabledtruefalse turns the preview off.
setupnoneA list of shell commands. Needs services.
servicesnoneA list of services. Names are unique and match [a-z][a-z0-9-]*, at most 32 characters. Ports are unique.
services[].commandrequiredAt most 4096 characters.
services[].portrequired1024 to 65535. Factorial refuses port 49983. Choose a port below 32768 (see the execution rules).
services[].ready.path/Starts with /. At most 256 characters, no whitespace.
services[].ready.timeout120sFrom 1s to 30m.
envnoneNames are upper-case environment names. Factorial refuses PORT, HOME, USER, PATH and LANG. A value can use {secrets.KEY}, and only for a secret whose name starts with PREVIEW_.
stopAfter30mFrom 1m to 55m.
pagenoneThe plan page. Do not combine it with services.

setup, env and stopAfter need services. Sync refuses a block that breaks a rule and names the rule. For the other fields of a process, see Processes.

Execution rules

  • Setup runs from the checkout as the user of the agent, without env. Services run from the checkout as a separate preview user, with env and with HOME set to /home/preview. Both users can write to the checkout. Each setup command has its own shell, so a cd or an export does not carry into the next command.
  • Only the declared service ports are public. A server on any other port is not reachable from outside the sandbox.
  • While the agent runs, or while an interactive session is open, services reach only the loopback address, and only on the declared ports. A local server on an undeclared port does not answer them. Declare every port that the app uses. Connect to a local database through a Unix socket. When neither is live, services have normal network access.
  • Choose ports below 32768. The change to this mode cuts every connection on a declared port. A port in the higher range can be the local port of another connection.
  • The root of the checkout is sticky. A service cannot rename or delete an entry that the agent owns there, for example a dist/ that setup built. Let setup and services write to separate directories, or let the service own what it rebuilds.
  • The agent cannot reopen for writing a regular file that the preview created in the root of the checkout. Keep generated files in subdirectories.
  • Setup runs until it succeeds. Later starts in the same sandbox skip it. Make a partial setup safe to retry. Setup can also run while the session of the agent uses the same checkout, so make it safe to run then.
  • Services start at the same time after setup. A service must cope with a dependency that is not ready.
  • The runtime sets PORT for each service. Do not set it in env.
  • Readiness checks use 127.0.0.1 and accept any HTTP answer below 500, including 403. Test the public URL as well: a passing check does not prove that the browser can use the app.
  • stopAfter counts from a start or Restart by a person, from the end of the run, or from Keep running. It does not count while the run is live. The dashboard warns five minutes before it closes the preview.

A preview: block with no services and no page waits for a producer outside Factorial to publish a URL. That mode has no Start preview button.

Full-stack previews

An E2B runtime can start a frontend and a backend in one sandbox. It does not manage databases, containers, start order or migrations. The examples need scripts in your app. They are patterns, not tested deployments.

Every declared service needs an HTTP readiness check. Do not declare PostgreSQL on port 5432 as a service: the runtime cannot check that protocol. All services must pass before Factorial publishes any URL. The first service appears in the frame. The panel links to the others.

An external preview database. Create a disposable database first. Store its connection string as the project secret PREVIEW_DATABASE_URL. Test the real database driver from E2B: an egress host entry alone does not prove that the connection works. While the agent runs, or an interactive session is open, services have no outside network access and cannot reach this database. A preview that starts then fails its readiness check. Start it again after the session parks.

  preview:
    setup:
      - "npm ci"
      - "npm run build:shared"
    env:
      DATABASE_URL: "{secrets.PREVIEW_DATABASE_URL}"
      API_INTERNAL_URL: "http://127.0.0.1:3000"
    services:
      - name: web
        command: "bash scripts/preview-web.sh"
        port: 3001
        ready: { path: "/", timeout: 120s }
      - name: api
        command: "bash scripts/preview-api.sh"
        port: 3000
        ready: { path: "/health", timeout: 180s }
    stopAfter: 30m

The app must implement these parts:

  • build:shared builds the workspace packages. Change or remove it to match the app.
  • preview-web.sh starts the frontend on 0.0.0.0:$PORT. Its server proxies browser requests from /api/* to API_INTERNAL_URL. Browser code uses the public origin of the frontend.
  • preview-api.sh waits for the database, applies safe migrations, then starts the API on $PORT. Its /health returns 503 until the API and the database are ready, then 200.
  • The installation hosts and the database host must be in the egress list before the build starts.

DATABASE_URL and API_INTERNAL_URL are names that the app defines. Factorial gives the same env to setup and to every service. It does not keep database credentials from the other processes. It expands secret references but does not put public service URLs into env. Use separate data for previews that run at the same time: Factorial does not isolate an external database for each run.

A database inside the sandbox. Use a template that already has the database binaries. The operator chooses the template (Register a runtime). Setup and services have no sudo, so they cannot install system packages or manage a database service that root owns.

Keep the two HTTP services above, remove the external DATABASE_URL, and replace the API command:

      - name: api
        command: "bash scripts/preview-api-with-db.sh"
        port: 3000
        ready: { path: "/health", timeout: 180s }

The script must create the database storage if it is absent, start the database, wait for it, apply migrations and start the API. Keep the data in a directory that the preview user owns, outside the checkout, for example $HOME/preview-db. Keep the database sockets in such a directory too: a cold boot clears /run. Connect the API to the database through the socket, because a TCP port that is not declared does not answer while a run is live.

The wrapper runs at every preview start. It must supervise both processes and handle shutdown. Do not start the database in setup: a successful setup does not run again in the same sandbox. A stop sends SIGTERM to every process of the preview user, and SIGKILL after ten seconds if needed. The processes of the agent keep running. The database must recover from an abrupt stop. Files survive a successful park and are gone when Factorial releases the sandbox. There is no backup of the database.

Public access applies to the whole sandbox, not to a service. No flag makes a service private, and a process that you leave out of services is not protected. Test every listener, and use only disposable data.

Step 5. Check the branch instructions of the agent

Read system-prompt.md and instructions.md of the build agent. Compare its checkout and pull request commands with the gates and postconditions of the process.

For a stacked process whose spec approval has merge: false, check that:

  • the agent fetches spec/<ticket>-<slug> before it looks for spec files;
  • it creates impl/<ticket>-<slug> from that spec branch and reuses the slug of the plan agent;
  • its pull request targets the spec branch and not main;
  • a revision reuses the implementation branch and handles both pr_review_comments and change_request inputs.

Keep workspace.base on a ref that resolves, such as main. Do not put {slug} there. The agent moves to the spec branch after the checkout. A process that merges the spec first needs instructions that match that workflow.

Step 6. Sync and test

  1. Open a pull request into factorial with the configuration and prompt changes.
  2. Merge it with a merge commit or a squash. Then select Sync from repo in the dashboard. In the editor, Commit and sync does both. See Snapshot the config.
  3. Check that the sync succeeded.
  4. Admit a new ticket, with the entry label and assignee rules of the process.
  5. Move it through the gates until the build run starts.
  6. During the session, open Ticket, then Activity, then the build run, then Output. Check that Terminal and Preview open side by side.
  7. Wait until the preview is ready. Test the page, the actions that need the API, and Open in new tab.
  8. Send Request changes at the gate. Check that the drawer of the rework shows the preview still up.
  9. Leave the preview idle. Check the warning at five minutes and Keep running.
  10. Select Restart, then Stop preview, and start it again.

The preview starts by itself when the pane opens on the latest attempt. The pane decides once, on the first answer it gets, and starts the preview only if it is not running. A stop that you see later, while the pane stays open, stays until you select Start preview. Open the pane again on a stopped latest attempt, and it starts the preview again. Repeated start requests while it starts or serves return the current state and do not extend the deadline.

Test before the ticket is done. A done ticket serves no preview and stops the previews that are awake. A rework through the gate carries a ready preview over to its run. If less than 15 minutes of the hour of the sandbox is left, the preview starts again on the run of the rework instead. A failed build can still have a preview if its sandbox remains.

Each ticket keeps the configuration and prompts that were current at its admission. A retry of an old ticket does not use a later sync. A warning about uncommitted changes means the preview can show files that are not in the pull request.

Publish a plan page

A plan page gives a reviewer a one-page summary of the proposal of a step, beside the transcript of the run. The agent writes one file: what changes where, diagrams, risks and links to the spec. The file can be HTML or Markdown. A file with any other extension shows as plain text. Use a plan page on a step that makes a document and not an app.

What a plan page does

  • The agent gets the instructions. The first turn of the run has a Page section. It gives the path, the page branch, the size limit and the exact commands that publish the page.
  • The page never merges. The commands commit the file as the only file on the page branch, and the page branch never gets a pull request. The own branch of the agent stays as it was, and the commands remove the file from the checkout. Later agents read only the branches that work merges into, so they never read a design that was only proposed.
  • Factorial records it when the run ends. It reads the page branch on GitHub and records where the page landed for that run. Each attempt keeps its own page. A later push does not replace the page of an earlier attempt.
  • The page is inert. The API serves it with no script and loads nothing from outside the page. The frame and Open in new tab use a signed link for that one run. The link expires within two hours. With Google sign-in on, the link also needs a Google session. A reader who signed in with the API token sees Sign in with Google in place of the page. Links in the page open in a new tab. For the reasons, see Previews and served pages.
  • No sandbox stays. Factorial keeps no sandbox for a page, so there is no Start preview or Stop preview. Every runtime gives the agent the Page section.
  • It blocks nothing. A run with no page succeeds or fails as it would without page:.

Do not type a credential into a page. When Google sign-in is off, do not share an Open in new tab link: anyone who has it can read the page until it expires.

Step 7. Configure the page

Edit .factorial/processes/process-<name>.yaml on the factorial branch. Add preview: to the agent step whose proposal you review, usually the plan step, next to workspace and onSuccess. This block assumes a plan step that pushes spec/{ticket}-{slug}. It is not a complete step.

  - name: plan
    kind: agent
    agent: plan-agent
    workspace:
      repo: "{repo}"
      base: main
      branch: "spec/{ticket}-{slug}"
    preview:
      page:
        branch: "pages/{ticket}-{slug}"
        path: "summary.html"
  1. Put {ticket} in branch. Two tickets that share a page branch show each other's page.
  2. Name branch apart from the workspace.branch of every agent step. Use a prefix such as pages/ that no step branch uses.
  3. Make path a relative path inside the repository. Its extension sets how the page shows: .html as written, .md as rendered Markdown, and any other extension as plain text.
  4. Use only letters, digits, ., _, -, /, {ticket} and {slug} in both fields. A path segment must not start with -. branch must not end in /. path must not start with / or contain ...
  5. Do not add services, setup, env or stopAfter to the same block.

Page rules

  • {slug} is the slug that the run chose for its own workspace.branch. The agent puts it into the publish commands. If Factorial cannot tell which slug the run chose, it matches {slug} as *.
  • The parser refuses a page branch that equals workspace.base or the branch of the step. It also refuses a branch that overlaps the branch of any agent step.
  • The publish commands refuse a file over 1 MB (1,048,576 bytes). The API frames nothing larger.
  • Each publish adds a commit on top of the page branch, so its history keeps the page of every attempt.
  • Factorial never deletes a page branch. Delete old page branches yourself.
  • enabled: false turns the page off. The first turn of the run then has no Page section.
  • A page never blocks the step. To fail the step when there is no page, add file_exists to its postconditions:
        - file_exists("summary.html", "pages/{ticket}-*")

Step 8. Give the agent the page skill

The Page section tells the agent where the page goes and how to publish it. The spec-page skill tells it what to put on the page. That is the change in one paragraph, the risks, the links, and the smallest views that make each point clear. A view is pseudocode, a call tree, a component tree, a file tree, a diff or a diagram. The page runs no scripts, so a diagram is inline SVG or text and never Mermaid.

  1. Check that .factorial/skills/spec-page/SKILL.md exists. The first commit of the editor's Simple template writes it.
  2. Add spec-page to skills.shared in the agent.yaml of the step's agent.
  3. Read the system-prompt.md and instructions.md of the agent.
  4. Remove any instruction to commit the page to the own branch of the agent, or to publish it another way.
skills:
  shared:
    - project-constitution
    - spec-page

The skill is optional. Without it, the agent still gets the path, the limits and the commands. Do not copy the publish commands into a prompt or a skill: the first turn gives them. Change the content list of the skill to suit the repository, or rewrite it for a step that is not a plan step. For skills in general, see Skills.

Step 9. Let the App push page branches

The agent pushes the page branch as the GitHub App, directly, with no pull request. This uses the Contents write permission of the App (Set the permissions). A branch rule that matches the page branch can refuse the push.

  1. Open Settings, then Rules, then Rulesets, and Settings, then Branches, in the repository.
  2. Find each branch rule whose target matches the page branch, such as pages/* or all branches.
  3. Exclude the page branches from that rule, or remove the rule that stops a direct push.

These rules stop a direct push:

  • Restrict creations
  • Restrict updates
  • Require a pull request before merging
  • Require status checks to pass
  • Require signed commits

Restrict deletions and Block force pushes are safe. The publish commands add a commit on top and never delete the branch.

Step 10. Sync and test

  1. Open a pull request into factorial with the process, skill and manifest changes.
  2. Merge it, then select Sync from repo in the dashboard.
  3. Admit a new ticket, with the entry label and assignee rules of the process.
  4. Wait for the run of the step to end. Factorial looks for the page only after the run ends.
  5. Open Ticket, then Activity, then the plan run, then Output. The Preview section is open.
  6. Check that the toolbar names the file and its commit, such as summary.html at 1a2b3c4.
  7. Test the page, Reload, Open in new tab and View on GitHub.
  8. Click a link in the page. It must open in a new tab.

Until the run ends, the Preview section shows The page comes when attempt N ends. Factorial looks up an interrupted run only when it ends, because its resume can still push the page.

When the gate waits on a successful run, Request changes starts rework through the onReject of the gate. Open the preview of the new run after the rework: each attempt shows its own page. A ticket admitted before the sync gets no page.

When a preview fails

Find the symptom in the table. A correction to the process, a manifest, a prompt or a skill reaches a ticket only after you sync and admit a new ticket.

App previews

SymptomCheck and action
No Preview section in OutputCheck the enabled preview: block of the step in the pinned config of the ticket. Sync the correction and admit a new ticket.
No preview publishedThis toolbar text means only that no URL is available. Select Start preview if it shows. Read the status under it.
Preview not available yet, no start buttonCheck the pinned block for services or page. With neither, Factorial waits for an outside URL.
Renewing the sandboxE2B allows a sandbox one hour between pauses. The preview restarts by itself. Wait.
Stopped after it sat idleNobody kept it for stopAfter. Start it again.
The runtime cannot serve a previewCheck that the runtime has E2B isolation and that the project permits it (Step 2). Use a new run after the correction.
setup failed, ECONNRESET at a package registryCheck the egress hosts at sandbox creation. Add the missing hosts and use a new sandbox. If they are there, read the network errors.
A command, package or build output is missingCheck setup, the package manager, the workspace builds and the paths. Read the error and the log tail in the panel.
port … is held by another userAnother process holds a declared port, often a server that the agent left running. Stop it, or start the preview after the run ends.
A service cannot rename or delete a build outputThe agent owns that entry in the sticky root of the checkout. Write the output to a separate directory, or let the service own it.
The agent cannot rewrite a file that the preview madeKeep generated files in subdirectories.
A service did not answer before its timeoutCheck the process log, the port, the readiness path, the environment and the dependencies. Raise the timeout only for a healthy but slow start.
Ready, but the browser shows 403 or API errorsCheck the allowed hosts, the public API URLs, CORS, HTTPS and database access. The local readiness check does not cover them.
The sandbox is no longer keptFactorial keeps a sandbox until its ticket exits or its retention passes. The retention is seven days by default. Use a newer run or a new ticket.
The ticket is doneA done ticket serves no preview. Use a new ticket.
A later attempt continues in this workspaceAn interactive rework resumes in the sandbox of the previous attempt, and the preview carries over to the newer run. Open the preview of the newer run.
Three previews or open sessions are already awakeStop another preview or close an open session, then try again. A preview beside a live run does not count.

Plan pages

SymptomCheck and action
Sync refuses the page blockThe message names the rule. Correct the branch or the path as Step 7 says.
The page comes when attempt N ends does not changeThe run has not ended, or it is in an interrupt.
Getting the page from GitHub does not changeFactorial waits while the ticket has the exit label, and tries again while GitHub fails.
No page publishedThe agent did not push the file. Read the transcript of the run for the output of the publish commands. Check the branch rules of the page branch (Step 9).
… bytes; the limit is 1048576 in the transcriptThe page is over 1 MB, so the commands did not push it. Draw diagrams as SVG and not as large data: images.
More than one page matchesTwo branches or files match the patterns. Delete the stale page branch, or make the path more specific.
Not published in this attemptThe rework did not publish a new page. The page branch still holds the page of an earlier attempt.
Unchanged since attempt NThe rework published the same file. If the spec changed, ask for changes to the page.
The page could not be looked upGitHub failed until the next attempt started, or the commit tree was too large to list.
The page is over 1 MBThe page is not framed. Make it smaller, or read it on GitHub.
The frame shows the page is not UTF-8 textA Markdown or plain-text file is not UTF-8 text, for example an image. Write the page as UTF-8 text, or read it on GitHub.
The frame shows the page renders too large to showThe Markdown makes too large a page, for example a wide table with many rows. Make the tables smaller, or read the page on GitHub.
The page shows as plain textThe path does not end in .html or .md. Change path in the page block.
The page has no linkThe API token is not set. Check that FACTORIAL_API_TOKEN is not empty in the .env of the deployment (Write the .env file), and start the container again. Read the page on GitHub meanwhile.
A chart, image or font does not showThe page loads nothing from outside itself. In an HTML page, use inline SVG, a <style> element and data: URIs. In a Markdown page, use data: images: raw HTML such as <svg> shows as text.
A link opens inside the drawerThe link in the HTML page has no target="_blank". Use Open in new tab, and correct the skill or the page. Links in a Markdown page always open a new tab.

Which changes need a new run

ChangeWhen it takes effect
Process YAML, agent manifests, prompts or skillsAfter the merge and sync, for a newly admitted ticket.
Project egress hostsAt sandbox creation. A restart of an existing preview keeps its original list.
A successful setupOnce for each sandbox. A restart of the services does not run it again.
A new build runIt gets its own preview. The sandbox of the previous run stays under the same retention rule. An interactive rework continues in the previous sandbox and carries its preview over.
Branch rules for page branchesAt the next push. A run whose push failed does not push again: request changes, or admit a new ticket.
A new run of a page stepIt publishes its own page. The preview of each attempt shows the page of that attempt.

Schema checks cover the structure of the configuration and the gate relations. They do not test downloads, app startup, page publishing or browser behavior. Complete Step 6 or Step 10 before you call the set-up working.