> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Configure tasks to run on-demand workloads and AI Agents

> Run on-demand workloads (AI agents, batch jobs) alongside your apps and services, triggered by an API call.

export const MetaImageVersion = ({language, version}) => {
  const [selectedVersion, setSelectedVersion] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const isComposable = language === 'composable';
  const STORAGE_KEY = isComposable ? 'upsun_composable_cache' : 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = isComposable ? 'https://meta.upsun.com/composable' : 'https://meta.upsun.com/images';
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  useEffect(() => {
    if (!language) {
      setLoading(false);
      return;
    }
    setLoading(true);
    setError(null);
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) return cachedData;
          }
        } catch (error_) {
          console.error('Failed to load from cache:', error_);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (error_) {
            console.error('Failed to refresh cache metadata:', error_);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    fetchData().then(data => {
      if (!data) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      const imageData = isComposable ? data : data[language];
      if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      let versionName = null;
      if (version && version !== 'latest') {
        versionName = (version in imageData.versions) ? version : null;
      } else {
        versionName = findHighestVersion(imageData.versions);
      }
      setSelectedVersion(versionName);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaImageVersion error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [language, version]);
  if (loading) return <span>…</span>;
  if (error) return <span title={error}>⚠ unavailable</span>;
  if (!selectedVersion) return <span>No version found</span>;
  return <span>{selectedVersion}</span>;
};

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

<Note>
  Tasks are in **prerelease**. To request this feature on your project, [open this prepopulated support ticket](https://console.upsun.com/-/users/-/tickets/open?isGeneral=true\&category=feature_request_cat\&priority=normal\&subject=Enable%20task%20containers%20\(prerelease\)%20on%20my%20project\&description=Hello%2C%20I%27d%20like%20to%20enable%20task%20containers%20on%20my%20project.%0A%0AProject%20ID%3A%20%3CPROJECT_ID%3E) and add your project ID before submitting.
</Note>

A **task** is an on-demand, run-to-completion workload defined alongside your applications and services in `.upsun/config.yaml`.
When triggered, a task container is injected into your environment's cluster, runs a single command, and is removed when the command exits.

## When to use a task

A task is the right choice when you need:

* **A background AI agent session** — for example, an agent that reads your codebase, calls tools such as shell commands or database queries, and writes the results back to your project.
* **A one-time job that needs service access:** database maintenance, a data export, a report.
* **A batch job triggered from your application** rather than on a schedule.

For a working example, see the [Upsun performance agent](/docs/get-started/ai/upsun-performance-agent), which runs as a task to analyze observability data and generate a report.

A task is *not* the right choice for:

* **A long-running daemon**: use a [worker](/docs/configure-apps/image-properties/workers#workers).
* **A scheduled job**: use a [cron](/docs/configure-apps/image-properties/crons).
* **Serving HTTP traffic**: tasks are not routable and cannot be used as an upstream in `routes:`.

## How tasks compare to other workloads

|                         | Application    | Worker               | Cron                         | Task                         |
| :---------------------- | :------------- | :------------------- | :--------------------------- | :--------------------------- |
| Lifecycle               | Always running | Always running       | Scheduled, run-to-completion | On-demand, run-to-completion |
| HTTP exposure           | Yes            | No                   | No                           | No                           |
| Has its own image/build | Yes            | No (shares app slug) | No (shares app slug)         | Yes                          |
| Triggered by            | Deploy         | Deploy               | Schedule                     | API call                     |
| Blocks deployments      | N/A            | N/A                  | Cancelled on deploy          | Cancelled on deploy          |

## Define a task

Tasks are declared at the top level of `.upsun/config.yaml`, alongside `applications:` and `services:`.

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: php:{{version:php:latest}}
          relationships:
            database: {}
          authorizations:
            - type: task # allow the app to trigger and operate this task
              resource: myagent
              action: operate
            - type: env # allow the app to access the environment API (read-only)
              action: view
          # ...

      services:
        database:
          type: postgresql:{{version:postgresql:latest}}

      tasks:
        myagent:
          source:
            root: /agent
          type: nodejs:{{version:nodejs:latest}}
          run:
            command: node agent.js
            timeout: 3600
          authorizations: # allow the task to access the environment API (read-only)
            - type: env
              action: view
          hooks:
            build: npm ci
          relationships:
            database: {}
            app: "myapp:http"
    `
  }
</DynamicCodeBlock>

### Required fields

| Field         | Description                                                                                                                                                                                                 |
| :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`        | The runtime image; same syntax as an application and the `type` in the preceding example.                                                                                                                   |
| `run.command` | The command to execute. Runs to completion, not as a daemon. To run initialization steps that need service access, place them at the start of this command — for example: `node setup.js && node agent.js`. |

### Common optional fields

| Field                                                                    | Description                                                                                                                                                                                                                                                                                                        |
| :----------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`source.root`](/docs/configure-apps/image-properties/source)            | Subdirectory containing the task's code. Defaults to `/`.                                                                                                                                                                                                                                                          |
| `run.timeout`                                                            | Maximum duration in seconds. Defaults to `3600` (one hour); max value `86400` (one day). On timeout, the platform sends `SIGTERM`, then `SIGKILL` after a grace period.                                                                                                                                            |
| [`authorizations`](/docs/configure-apps/image-properties/authorizations) | Declared on a **task**: API permissions granted to the task container at runtime. Declared on an **application**: grants the app permission to trigger tasks and call the environment API.                                                                                                                         |
| [`hooks.build`](/docs/configure-apps/image-properties/hooks)             | Command run during the environment build phase, producing a slug reused across runs.                                                                                                                                                                                                                               |
| [`relationships`](/docs/configure-apps/image-properties/relationships)   | Connections to services and applications in the same cluster.                                                                                                                                                                                                                                                      |
| [`mounts`](/docs/configure-apps/image-properties/mounts)                 | Writable directories. `instance` and `tmp` mounts are reset between runs; use a `storage` or `service` mount to persist data across runs.                                                                                                                                                                          |
| [`variables`](/docs/configure-apps/image-properties/variables)           | Environment variables defined in your config file. Only use this for non-sensitive values — set secrets such as passwords and API keys using the [API or CLI](/docs/development/variables/set-variables) to keep them out of version control, for example: `upsun variable:create --name MY_SECRET --sensitive 1`. |

### Fields that don't apply

[`web`](/docs/configure-apps/image-properties/web), [`workers`](/docs/configure-apps/image-properties/workers), and [`crons`](/docs/configure-apps/image-properties/crons) have no meaning for tasks. Tasks have no HTTP router, are not long-running, and are not scheduled.

## Relationships

A task can declare relationships to applications and services in the same environment:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      tasks:
        myagent:
          relationships:
            database: {}      # e.g. PostgreSQL service
            cache: {}         # e.g. Redis service
            app: "myapp:http" # application`
  }
</DynamicCodeBlock>

Inside the task container, these relationships appear in `PLATFORM_RELATIONSHIPS` exactly as they do for applications.
The task can read from the database, write to the cache, and POST results back to the app over HTTP.

**Relationship direction is one-way.**
Applications and other tasks cannot declare a relationship *to* a task, because a task exists in the cluster only while a task is running.
If you need the app to receive results from a task, the task should push them to the app, for example by using the <code style={{whiteSpace: 'nowrap'}}>app: "myapp:http"</code> relationship.

## Trigger a task

Tasks are triggered through the Upsun API (see the [task run endpoint reference](https://developer.upsun.com/api-reference/task/post-projects-environments-tasks-run)):

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  https://api.upsun.com/projects/{project}/environments/{env}/tasks/myagent/run
```

To pass run-time variables to the task, include a `variables` object in the request body:

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"variables": {"env": {"BATCH_SIZE": "100", "TARGET": "prod"}}}' \
  https://api.upsun.com/projects/{project}/environments/{env}/tasks/myagent/run
```

`$TOKEN` is a short-lived access token obtained from your API token. See [Authentication](/api/rest/authentication) for the exchange process and [API tokens](/cli/api-tokens) for the steps to create one.

Each invocation creates an **activity** (the same mechanism used for deploys, backups, and crons) that gives you:

* A unique activity ID.
* Live status (`pending`, `in_progress`, `complete`, `cancelled`).
* Streamed logs through the existing activity logs endpoint.
* Cancellation through the `/cancel` endpoint on the activity.

## Trigger from application code

Declare which tasks the app is allowed to trigger using [**workload authorizations**](/docs/configure-apps/image-properties/authorizations) (the `authorizations` key), then request a short-lived token at runtime — no long-lived credentials required.

Steps 1–3 break down each part individually. For a complete implementation combining token retrieval, caching, and triggering, see [Sample implementation](#sample-implementation).

### 1. Declare the authorization

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          authorizations:
            - type: task
              resource: myagent
              action: operate`
  }
</DynamicCodeBlock>

### 2. Request a token and trigger the task

```bash theme={null}
token=$(curl http://localhost:8200/oauth2/token -d grant_type=client_credentials | jq -r .access_token)

curl -X POST -H "Authorization: Bearer $token" \
  "https://api.upsun.com/projects/$PLATFORM_PROJECT/environments/$PLATFORM_BRANCH/tasks/myagent/run"
```

### 3. Cache the token

Token caching is highly recommended if your application triggers tasks repeatedly. Without token caching,
every trigger makes a round-trip to the auth proxy. Request a new token only when the current one is about to expire.

By default, tokens expire after 60 seconds. The `x-token-ttl` header extends the lifetime up to 900 seconds (15 minutes) — set it to match your expected time between task triggers. For example, if your app triggers a task every 5 minutes (300 seconds), set `x-token-ttl` to slightly more than the trigger interval to account for network delays.

### Sample implementation

The following examples combine steps 1–3 into a single helper that handles authorization declaration, token retrieval, caching, and task triggering.

<CodeGroup>
  ```python title="Python" theme={null}
  import os
  import time
  import requests

  _cache = {}

  def _get_token():
      if _cache.get("expires_at", 0) - 30 > time.time():
          return _cache["token"]
      r = requests.post("http://localhost:8200/oauth2/token",
                        data={"grant_type": "client_credentials"},
                        headers={"x-token-ttl": "900"})  # 60–900 seconds
      r.raise_for_status()
      data = r.json()
      _cache["token"] = data["access_token"]
      _cache["expires_at"] = time.time() + data.get("expires_in", 900)
      return _cache["token"]

  def trigger_task(task_name, variables=None):
      token = _get_token()
      project = os.environ["PLATFORM_PROJECT"]
      branch  = os.environ["PLATFORM_BRANCH"]
      r = requests.post(
          f"https://api.upsun.com/projects/{project}/environments/{branch}/tasks/{task_name}/run",
          headers={"Authorization": f"Bearer {token}"},
          json={"variables": variables} if variables else None,
      )
      r.raise_for_status()
      return r.json()

  # Example: pass run-time variables
  trigger_task("myagent", variables={"env": {"BATCH_SIZE": "100", "TARGET": "prod"}})
  ```

  ```javascript title="Node.js" theme={null}
  const cache = {};

  async function getToken() {
    if (cache.expiresAt - 30_000 > Date.now()) return cache.token;
    const res = await fetch('http://localhost:8200/oauth2/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'x-token-ttl': '900', // 60–900 seconds
      },
      body: 'grant_type=client_credentials',
    });
    const data = await res.json();
    cache.token = data.access_token;
    cache.expiresAt = Date.now() + (data.expires_in ?? 900) * 1000;
    return cache.token;
  }

  export async function triggerTask(taskName, variables = {}) {
    const token = await getToken();
    const { PLATFORM_PROJECT, PLATFORM_BRANCH } = process.env;
    const url = `https://api.upsun.com/projects/${PLATFORM_PROJECT}/environments/${PLATFORM_BRANCH}/tasks/${taskName}/run`;
    const hasVars = Object.keys(variables).length > 0;
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        ...(hasVars ? { 'Content-Type': 'application/json' } : {}),
      },
      body: hasVars ? JSON.stringify({ variables }) : undefined,
    });
    return res.json();
  }

  // Example: pass run-time variables
  await triggerTask('myagent', { env: { BATCH_SIZE: '100', TARGET: 'prod' } });
  ```
</CodeGroup>

## Resources, variables, and access

Tasks follow the same model as applications:

* **Resources** (CPU, memory): configured by using `upsun resources:set` against the task name. Billing is per-second for the duration of each run.
* **Variables and secrets**: configured with `upsun variable:create --level environment` or by using the API. Sensitive variables are injected as environment variables at run time.
* **Access**: standard project roles apply. Project admins and contributors can change task definitions, trigger tasks, and view runs.

## Security

Tasks run in the same Linux Containers (LXC) as applications, with the same namespace isolation, capability dropping (removing unnecessary Linux process privileges), seccomp profile, cgroup limits, and network isolation.
Cross-project isolation and relationship-based access control apply unchanged. For details, see the [Project isolation](/docs/security/project-isolation) topic.

For tasks that run untrusted code (for example, an LLM-driven agent), consider pairing with [bubblewrap](https://github.com/containers/bubblewrap) inside the task container to restrict filesystem access, environment variables, and syscalls visible to the agent process.

## Known limitations

* **Cancelled on deploy.** A running task receives `SIGTERM` before any deploy on the same environment, followed by `SIGKILL` after a grace period of a few seconds. Catch `SIGTERM`, persist state to a service, and exit cleanly. The caller is responsible for retrying.
* **Concurrency cap.** Multiple task runs can execute in parallel up to a default limit of 3. Further triggers queue behind running ones.
* **No task-to-task relationships.** Nothing can declare a relationship *to* a task. An app and a task can share a relationship to the same service or a network file mount.
* **Fresh filesystem every run.** `instance` and `tmp` mounts are reset between runs. Use a `storage` or `service` mount for files that must survive across runs.
* **Requires at least one application.** A project with only tasks and services is not currently valid.
* **Minimal Console and CLI support.** Task activities appear in the activity feed, but there is no dedicated task UI as part of this Prerelease program.
