---
title: "Remote GPU and long jobs"
description: "Run GPU and long jobs on your own Modal, Replicate or RunPod account, or a local program, as workflows with progress, cancel, cost limits and results in files."
section: "interfaces"
status: "Building for 0.6 · ships as a experimental API"
spec: "compute"
canonical: "https://mainsdk.dev/docs/interfaces/compute"
updated: "2026-10-28"
---

# Remote GPU and long jobs

> Run GPU and long jobs on your own Modal, Replicate or RunPod account, or a local program, as workflows with progress, cancel, cost limits and results in files.

Status: Building for 0.6 · ships as a experimental API · Canonical: https://mainsdk.dev/docs/interfaces/compute · Updated: 2026-10-28

Run GPU and long jobs on your own Modal, Replicate or RunPod account, or a local program, as workflows with progress, cancel, cost limits and results in files.

> **Note:** Built in 0.6 and covered by tests on the real workflow engine. The Modal, Replicate and RunPod drivers are tested against in-repo fakes, not live accounts; the Modal call API is an assumed contract. The `local` driver reports progress when the program ends and is stopped only by its timeout.

## At a glance

- Status: Building for 0.6 · ships as a experimental API
- Phase: 2 (Cloud and Teams, 1.0)
- Milestone: 0.6 Cloud
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/compute.md
- Category: AI and agents

Calls:

- `compute.run(target, input, { files?, maxCost?, timeout?, agent?, idempotencyKey? })`
- `compute.get(jobId)`
- `compute.list({ target?, state?, cursor?, limit? })`
- `compute.cancel(jobId, { reason })`
- `compute.step(name, target, input, opts?)`
- `compute.callback(call, request)`

In main.yaml:

```yaml
compute:
  heartbeat: 60s
  poll: 15s
  results: { label: personal, max-size: 2GB, types: ["image/*"] }
  limits: { per-tenant: $50/month }
  targets:
    upscale:
      driver: replicate
      model: nightmareai/real-esrgan:f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa
      region: us
      timeout: 30m
      price: $0.0014/s
      max-cost: $2
```

Guarantees, whatever the driver:

- MainSDK never hosts models; jobs run on your provider account
- A job never costs more than its limit; the worst case is checked before submit
- Jobs survive restarts and can be cancelled at the provider
- Labelled data goes only to a target in the tenant's region


## Drivers

Pick one in main.yaml with `compute.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Modal | `modal` |  |  |
| Replicate | `replicate` |  |  |
| RunPod | `runpod` |  |  |
| Local program (system.run) | `local` | yes |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-COMPUTE-0001` | check | A target not declared under `compute.targets` |
| `MAIN-COMPUTE-0008` | check | A target isn't pinned: no model version, app or endpoint id |
| `MAIN-COMPUTE-0013` | check | A target has no `price` or `timeout`, or a timeout over 24h |
| `MAIN-COMPUTE-0002` | runtime | Refused before submit: the worst case (price times timeout) passes `maxCost`, a `compute.limits` ceiling or the agent's budget (402) |
| `MAIN-COMPUTE-0003` | runtime | The running cost reached `maxCost`; the job was cancelled at the provider |
| `MAIN-COMPUTE-0004` | runtime | No heartbeat for three periods; the job is `lost` and was cancelled at the provider |
| `MAIN-COMPUTE-0005` | runtime | The provider failed submit, status or cancel after retries (502) |
| `MAIN-COMPUTE-0006` | runtime | The job passed its timeout and was cancelled |
| `MAIN-COMPUTE-0007` | runtime | Labelled input to a target outside the tenant's region (403) |
| `MAIN-COMPUTE-0009` | runtime | An output is too large or of a type not in `compute.results.types` (413 or 415) |
| `MAIN-COMPUTE-0010` | runtime | No job with that id in the current tenant (404) |
| `MAIN-COMPUTE-0011` | runtime | Cancel of a finished job (409) |
| `MAIN-COMPUTE-0012` | runtime | A provider callback failed verification (400) |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| compute@1 contract and pinned targets | 2 | 0.6 | `compute#contract` |
| Jobs as workflow runs | 2 | 0.6 | `compute#run` |
| Wait on a job as one workflow step | 2 | 0.6 | `compute#step` |
| Cost limits before and during a job | 2 | 0.6 | `compute#cost` |
| Heartbeat, progress and lost jobs | 2 | 0.6 | `compute#heartbeat` |
| Results into files | 2 | 0.6 | `compute#results` |
| Region check for labelled input | 2 | 0.6 | `compute#region` |
| Verified provider callbacks | 2 | 0.6 | `compute#callbacks` |
| Modal, Replicate, RunPod and local drivers | 2 | 0.6 | `compute#drivers` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

```yaml title="main.yaml"
compute:
  heartbeat: 60s
  limits: { per-tenant: $50/month }
  targets:
    upscale:
      driver: replicate
      model: nightmareai/real-esrgan:f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa
      region: us
      timeout: 30m
      price: $0.0014/s
      max-cost: $2
```

```ts title="app/example.ts"
import { compute, type ComputeRunOptions } from "mainsdk/experimental/compute";

declare const image: NonNullable<ComputeRunOptions["files"]>[number];   // a file stored with `files`

// refused before submit if the worst case could pass $2
const { jobId } = await compute.run("upscale", { scale: 4 }, { files: [image], maxCost: "$2" });
const job = await compute.get(jobId);
```

## Calls

| Call | What it does |
| --- | --- |
| `compute.run(target, input, { files?, maxCost?, timeout?, agent?, idempotencyKey? })` | Submits a job to a target; refuses it first if the worst-case cost passes the limit. |
| `compute.get(jobId)` | Returns a job's state, progress, cost and result. |
| `compute.list({ target?, state?, cursor?, limit? })` | Pages through the current tenant's jobs. |
| `compute.cancel(jobId, { reason })` | Cancels a job at the provider and records why. |
| `compute.step(name, target, input, opts?)` | Runs a job as a step inside a workflow and returns its result. |

## Configuration

```yaml title="main.yaml"
compute:
  heartbeat: 60s        # no sign of life for three of these and the job is lost
  poll: 15s             # how often the workflow asks the provider
  results: { label: personal, max-size: 2GB, types: ["image/*", "video/*", "audio/*", "application/json"] }
  limits: { per-tenant: $50/month, total: $500/month }
  targets:
    upscale:
      driver: replicate      # modal | replicate | runpod | local
      model: nightmareai/real-esrgan:<64 hex>
      region: us             # required unless driver is local
      timeout: 30m           # at most 24h
      price: $0.0014/s
      max-cost: $2
      retries: 0             # 0 to 5
```

A target with no price or timeout stops the app from starting (`MAIN-COMPUTE-0013`). A `local` target names a `program` that is also declared under `permissions.processes`.

## What you get

```text
compute.run(target, input, { files?, maxCost?, timeout?, agent?, idempotencyKey? })
compute.get(jobId)
compute.list({ target?, state?, cursor?, limit? })
compute.cancel(jobId, { reason })
compute.step(name, target, input, opts?)   // as one step of a workflow
compute.callback(call, request)            // verify a provider callback and wake the run
```

A job is `queued`, `running`, `completed`, `failed`, `cancelled` or `lost`. It is a run of the built-in workflow `mainsdk.compute` (check, submit, poll, fetch, finish), so it survives a restart. Progress is published on the realtime channel `compute:<jobId>` and the cost on the meter metric `compute.cost`, in micro-dollars.

## Examples

### Cancel a job and read its result

```ts title="app/jobs.ts"
import { compute } from "mainsdk/experimental/compute";

export async function finish(jobId: string) {
  const job = await compute.get(jobId);
  if (job.state === "running") await compute.cancel(jobId, { reason: "no longer needed" });
  return job.result;
}
```

### Wait inside a workflow

```ts title="app/flows.ts"
import { compute } from "mainsdk/experimental/compute";

export async function upscaleStep() {
  return compute.step("upscale-step", "upscale", { scale: 4 });
}
```

## Errors and checks

| Code | When | Fix |
| --- | --- | --- |
| `MAIN-COMPUTE-0001` | A target is not declared under `compute.targets` (also a check on a literal in code) | Declare it in main.yaml, or fix the name |
| `MAIN-COMPUTE-0002` | The worst case (price times timeout) passes `maxCost`, a limit or the agent's budget | Raise the limit on purpose, or shorten the timeout |
| `MAIN-COMPUTE-0003` | The running cost reached `maxCost`; the job was cancelled | Raise `maxCost` or fix the job |
| `MAIN-COMPUTE-0004` | No sign of life for three heartbeats; the job is lost | Check the provider; set `retries` |
| `MAIN-COMPUTE-0005` | The provider failed or did not answer | Retry later |
| `MAIN-COMPUTE-0006` | The job passed its timeout | Raise `timeout` or shorten the job |
| `MAIN-COMPUTE-0007` | Labelled input and a target outside the tenant's region | Use a target in the tenant's region |
| `MAIN-COMPUTE-0008` | A target is not pinned (check) | Add the version hash, app and function, or endpoint id |
| `MAIN-COMPUTE-0009` | An output is too large or of a type not allowed | Adjust `results` on purpose |
| `MAIN-COMPUTE-0010` | No job with that id in this tenant | Use a job id from this tenant |
| `MAIN-COMPUTE-0011` | Cancel of a finished job | Nothing to cancel |
| `MAIN-COMPUTE-0012` | A provider callback failed verification | Check the webhook secret |
| `MAIN-COMPUTE-0013` | A target has no price, no timeout, or a timeout over 24h | Give it a price and a timeout of 24h or less |

## Swapping drivers

```yaml title="main.yaml"
compute:
  targets:
    upscale:
      driver: runpod   # was replicate
      endpoint: abc123
```

Your calls stay the same. See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## Testing

`FakeComputeProvider`, `ParkingFlows` and `computeConformance` come from `mainsdk/testing`. See [Run a GPU job](https://mainsdk.dev/docs/guides/run-gpu-jobs) and [Test your app](https://mainsdk.dev/docs/guides/testing).

## Security and privacy

- MainSDK never hosts models; jobs run on your provider account, and a driver declares that it never trains on inputs.
- Output is fetched only from the driver's declared hosts, then stored through [`files`](https://mainsdk.dev/docs/interfaces/files) with owner scope, scanning and limits.
- Labelled data goes only to a target in the tenant's region.
- `get`, `list` and `cancel` see only the current tenant's jobs; a callback only wakes the run, it never carries state.

## FAQ

**Can a job cost more than I set?** No. The worst case is reserved before submit and a running job is cancelled when its cost reaches `maxCost`.

## Next

- [File uploads](https://mainsdk.dev/docs/interfaces/files): where the input comes from.
- [Workflows](https://mainsdk.dev/docs/interfaces/workflows): the engine a job runs on.
- [Limits and metering](https://mainsdk.dev/docs/guides/limits-and-metering): watch the spend.
