docs
Python SDKCore Concepts

App

Define GPU functions and local entrypoints with the urun App container

App is the entry point for defining GPU functions in urun.

Every urun program starts with creating an App. The App instance is a container that holds your remote functions and local entrypoints. You register functions with the @app.function() decorator (Python) or invoke them as methods on the app handle (TypeScript), and urun handles provisioning, execution, and teardown on remote GPUs.

from urun import App

app = App("liveavatar")
import { App } from '@urun-sh/core'

const app = App('liveavatar', { baseUrl: 'https://urun.sh', orgId: 'your-org-id' })

The name identifies your app in logs, the console, and deployment. It must be unique within your account.

Function decorators

The @app.function() decorator registers a function to run on remote GPUs. The gpus parameter specifies the hardware — see Compute for the full specification format.

from urun import App, store
from urun.core import Dependencies, Credentials

app = App("liveavatar")

DEPS = Dependencies(
    python=["torch>=2.5.0", "transformers", "diffusers"],
)

CREDENTIALS = Credentials(
    env=["HF_TOKEN"],
)

@app.function(
    gpus="h100:8",
    deps=DEPS,
    credentials=CREDENTIALS,
    lease=3600,
)
def avatar(ctx, style: str = "photoreal"):
    """Drive a realtime avatar pipeline across 8x H100 GPUs."""
    # Session logic...

When a client opens avatar(style="anime"), the function body executes on remote H100 GPUs — not on your local machine. urun provisions the hardware, installs dependencies, injects credentials, and runs your code for the life of the session.

`ctx` is injected, not passed

If your function declares a first parameter annotated ctx: urun.Context, urun injects the execution context automatically — you never pass it yourself. Your own arguments come after it (def avatar(ctx: urun.Context, style: str = "photoreal")), and callers pass only those (avatar(style="anime")). ctx is optional: omit it if you don't need rank/device/streams. Streaming functions always declare it.

Key parameters:

  • gpus — GPU specification: "h100:8" for 8x H100, "a100:4" for 4x A100, or just 8 for default GPU type. This is deploy-time decorator config — it fixes the hardware shape when urun deploy registers the function. A same-named gpus passed to app.fn(...) at call time is just an inert argument and does not reshape the hardware.
  • deps — Python and system dependencies to install in the remote environment
  • credentials — Secrets to inject at runtime (e.g., HF_TOKEN)
  • env — Environment variables as a dict, for external inputs only (never behavior-tuning app config)
  • lease — Renewable session length in seconds, auto-renewed while the client stays connected
  • max_session_s — Hard, non-renewable session cap; past it the session ends with terminal reason max_session_age
  • session_idle_timeout — Idle kick in seconds (platform default 300; 0 opts out); the user is warned ~90s before
  • warm / snapshot — Idle replica floor and snapshot wakes: warm=0 scales to zero when idle, and (for GPU functions, multi-rank included) snapshots after the first validated boot so wakes restore instead of cold-booting
  • max_concurrency — Max concurrent sessions for the function (platform default 2); demand scales replicas up to this limit
  • session_kind"realtime" (default) or "doc" for docs+artifacts apps with no media lanes

The retired kwargs timeout, topology, gpus_per_replica, colocate_on_node, event_name, scaling, and gang raise a loud TypeError — cross-app calls via App.ref are best-effort by definition and have no declaration surface. The full kwarg-by-kwarg reference is at reference/app; the session-length story is explained in Sessions.

Streaming

@app.function() supports real-time streaming for interactive use cases like video generation or live inference. Inside any function, use ctx.stream() to create a pipeline video sink and compose stages with the | operator.

@app.function(gpus="h100:1", deps=DEPS, credentials=CREDS)
async def generate_video(ctx: urun.Context, prompt: str = "A serene landscape"):
    video = ctx.stream("video", codec="h264", fps=30)
    control = ctx.doc("control")

    pipeline = (
        ctx.source(generate_frames(control), device="cuda")  # reads control.desired.* inline
        | ctx.pace(fps=30)
        | video
    )

    await pipeline.run()

Key points:

  • Same decorator — streaming functions use @app.function(), the same decorator as every other function
  • Pipeline DSL — compose stages with ctx.source | ctx.pipe | ctx.pace | video; the video sink auto-converts float tensors to encoded frames
  • ctx.stream() — creates a pipeline video sink; clients subscribe by matching the stream name (e.g., session.stream("video") in TypeScript)
  • Reactive control — cheap config is a live control.desired.* read inline in the stage; structural change uses doc.bind. await pipeline.run() drives it continuously until disconnect
  • Bidirectional — clients send data back via session.doc("control").set(data), and the function reads it via ctx.doc("control") with listen() or get()

The Getting Started guide walks through a complete streaming example.

Class-based Apps

For apps with multiple related functions and shared context, use the @urun.app() class decorator:

import urun
from urun import store

@urun.app("my-studio")
class MyStudio:
    @urun.function(gpus="h100:1")
    def generate(self, ctx, prompt: str = "A quiet street at dawn"):
        # Live generation session...
        pass

    @urun.function(gpus="h100:1")
    def restyle(self, ctx, style_key: str):
        style = store.get(style_key)
        # Live restyle session...
        pass

Class-based apps are useful when you have a group of related functions that logically belong together — for example, generate, restyle, and upscale in one studio app. Each method gets the same decorator treatment as standalone functions.

Access the underlying App instance:

MyStudio._app.list_functions()  # ['generate', 'restyle']

When to prefer class-based over function-based:

  • Class-based — grouping related functions, shared configuration, cleaner namespacing
  • Function-based — simpler apps, single-purpose scripts, quick experiments

Cross-app calls: App.ref("other-app")

Functions on another app (registered by the same org) are called transparently through a ref — refs, never strings, cross app boundaries. App.ref("name") returns a proxy whose function attributes mint a real session on that app's function, the same primitive a browser gets:

from urun import App

ref = App.ref("qwen-image-edit")

session = ref.edit(prompt=prompt, frame=frame_ptr)   # a full session: session.doc(...) / session.stream(...)
edited = await ref.edit(prompt=prompt, frame=frame_ptr)  # or await it for the one-shot result

The same-app form is just the attribute: session = app.other_fn(...) mints a session on this app's own function. An unknown function name fails loudly at call time with the org-scoped lookup error. There is no ctx.call — transparent calls through the app/ref attribute are the one cross-function mechanism.

Walk-Through

This walk-through builds a small live-video app end-to-end: one function, one GPU shape, one stream a browser can attach to. For environment setup, see Getting Started.

from urun import App
from urun.core import Dependencies, Credentials
import urun

app = App("walkthrough-video")
store = app.store

DEPS = Dependencies(python=["torch>=2.5.0", "diffusers", "transformers"])
CREDS = Credentials(env=["HF_TOKEN"])


@store.cache(key="wan21-t2v-1.3b")
def load_pipeline(*, device):
    import torch
    from diffusers import DiffusionPipeline
    return DiffusionPipeline.from_pretrained(
        store.path("hf://Wan-AI/Wan2.1-T2V-1.3B"), torch_dtype=torch.bfloat16
    ).to(device)


@app.function(gpus="h100:1", deps=DEPS, credentials=CREDS, warm=0)
async def generate(ctx: urun.Context, prompt: str = "A serene landscape"):
    """Generate live video, steered by the client while it plays."""
    pipe = load_pipeline(device="cuda")
    control = ctx.doc("control")
    video = ctx.stream("video", codec="h264", fps=20)

    def frames(*, stop_event, config):
        while not stop_event.is_set():
            out = pipe(prompt=control.desired.prompt.value or prompt, num_frames=17)
            yield out.frames[0]

    await (ctx.source(frames, device="cuda") | ctx.pace(fps=20) | video).run()

Deploy it:

urun deploy walkthrough.py

Expected output:

  Provisioning h100:1 on cloud backend...
  Installing dependencies: torch>=2.5.0, diffusers, transformers
  Injecting credentials: HF_TOKEN

App deployed: walkthrough-video (generate)

Opening app.generate(prompt="A neon city in the rain") from a client mints a live session: the pipeline loads once through @store.cache (and stays resident for every later session), frames stream to the browser over the video stream, and the client steers the prompt mid-flight through the control doc — no restarts, no re-provisioning. With warm=0 the app scales to zero when nobody is connected and wakes from a snapshot.

Next: Compute

Compute defines the hardware your function runs on and how long its sessions live — GPUs, CPUs, memory, session length, and warm capacity.

Compute

Related:

On this page