Llim.run

Asset Storage

Asset Storage is the binary store that backs every Limrun instance. It lets you:

  1. Pre-install an app build (Android, iOS, iPadOS or watchOS) at boot so the embedded simulator starts with the app ready.
  2. Hot-deploy a fresh build into a running embedded session on rebuilds.

For how to connect platform users to those instances, see Embed the simulator.

What are Assets?

An asset is a single installable binary, such as an iOS simulator build (.app, .app.zip, or .app.tar.gz) or an Android APK (.apk).

interface Asset {
  id:                string;
  name:              string;
  displayName?:      string;
  md5?:              string;             // present only once bytes are uploaded
  os?:               'ios' | 'android';  // unset = usable on either platform
  signedDownloadUrl?: string;            // returned when includeDownloadUrl=true
  signedUploadUrl?:   string;            // returned when includeUploadUrl=true
}
FieldMeaning
idStable auto-generated identifier.
nameCustomizable identifier. Names need to be unique globally in your org. To organise for multi-tenancy, use prefixes.
displayNameOptional human-readable label surfaced in the console; falls back to name.
md5Set once the bytes have been uploaded. Unique hash used by getOrUpload to skip re-uploads for duplicates and by installApp to enable server-side caching.
osLimits which platform can install the asset. Useful when the same logical name (e.g. acme/my-app-v1.2.3) covers both an iOS sim build and an Android APK.
signedDownloadUrl / signedUploadUrlTime-limited URLs. Only returned when you ask for them on list/get, or always on getOrCreate.

Configuration entries on initialAssets

Stored assets are always binaries. That's the only thing the Asset record models, and there's no kind field on it. But spec.initialAssets accepts a second flavour of entry, kind: 'Configuration', which carries an instance-level setting instead of pointing at Asset Storage. The kind field lives on the initialAsset shape, not on the Asset record.

The only configuration today is ChromeFlag, used to enable Chrome's command-line on non-rooted Android emulators so Playwright can attach. SDK-only: @limrun/cli doesn't expose a --chrome-flag flag, so this entry has to come from the SDK directly.

spec: {
  initialAssets: [
    {
      kind: 'Configuration',
      configuration: {
        kind: 'ChromeFlag',
        chromeFlag: 'enable-command-line-on-non-rooted-devices@1',
      },
    },
  ],
}
limrun.AndroidInstanceNewParamsSpecInitialAsset{
    Kind: "Configuration",
    Configuration: limrun.AndroidInstanceNewParamsSpecInitialAssetConfiguration{
        Kind:       "ChromeFlag",
        ChromeFlag: "enable-command-line-on-non-rooted-devices@1",
    },
}
{
    "kind": "Configuration",
    "configuration": {
        "kind": "ChromeFlag",
        "chrome_flag": "enable-command-line-on-non-rooted-devices@1",
    },
}

A kind: 'Configuration' entry has no MD5, no signed URLs, and no assets.list representation. It exists only on the instance spec. The rest of this page is about kind: 'App' entries, which are the ones actually backed by Asset Storage.

Upload an asset

Two paths:

getOrUpload

getOrUpload computes the local file's MD5 hash, checks Asset Storage for an existing entry with the same name + same MD5, and skips re-upload if it matches.

This helper ships in TypeScript and Go SDKs. Python users have to do the two steps manually with getOrCreate (shown in the next subsection).

lim asset push ./build/MyApp.app.tar.gz -n my-app-v1.2.3.tar.gz
import Limrun from '@limrun/api';

const lim = new Limrun({ apiKey: process.env['LIM_API_KEY'] });

const asset = await lim.assets.getOrUpload({
  path: './build/MyApp.app.tar.gz',
  name: 'my-app-v1.2.3.tar.gz',     // optional, defaults to the file's basename
});

// asset.id, asset.name, asset.signedDownloadUrl, asset.md5
asset, err := lim.Assets.GetOrUpload(ctx, limrun.AssetGetOrUploadParams{
    Path: "./build/MyApp.app.tar.gz",
    Name: param.NewOpt("my-app-v1.2.3.tar.gz"), // optional
})
// asset.ID, asset.Name, asset.SignedDownloadURL, asset.MD5

If the file's MD5 already matches an existing upload, getOrUpload returns immediately with the existing asset's URLs and no bytes are transferred.

For example, Limrun's automatic PR preview GitHub Action uses it like this:

# Each PR in each repo gets its own asset slot, deletable on PR close.
# Naming convention: preview/<owner>/<repo>/pr-<number>-<platform>
lim asset push ./build/MyApp.app.tar.gz \
  -n "preview/${OWNER}/${REPO}/pr-${PR_NUMBER}-ios"

(The action itself rolls the build and upload into a single lim xcode build --upload "preview/…" step (see Build with remote Xcode), but the naming convention is the same.)

getOrCreate

When you need finer control, for example for uploading from a worker, from a different host, from a streaming source, or letting the end user's browser PUT directly to the storage URL, use the lower-level getOrCreate and PUT to the returned signedUploadUrl yourself.

const asset = await lim.assets.getOrCreate({ name: 'my-app-v1.2.3.tar.gz' });
// asset.id, asset.name, asset.signedUploadUrl, asset.signedDownloadUrl,
// asset.md5 (if an upload already exists)

if (!asset.md5) {
  const data = await fs.promises.readFile('./build/MyApp.app.tar.gz');
  await fetch(asset.signedUploadUrl, {
    method: 'PUT',
    body: data,
    headers: {
      'Content-Length': data.length.toString(),
      'Content-Type':   'application/octet-stream',
    },
  });
}
asset, err := lim.Assets.GetOrCreate(ctx, limrun.AssetGetOrCreateParams{
    Name: "my-app-v1.2.3.tar.gz",
})
// asset.SignedUploadURL, asset.SignedDownloadURL, asset.MD5 (if exists)

if asset.MD5 == "" {
    data, _ := os.ReadFile("./build/MyApp.app.tar.gz")
    req, _ := http.NewRequest("PUT", asset.SignedUploadURL, bytes.NewReader(data))
    req.Header.Set("Content-Type", "application/octet-stream")
    req.ContentLength = int64(len(data))
    _, err := http.DefaultClient.Do(req)
}
import hashlib
import requests
from limrun_api import Limrun

client = Limrun()  # picks up LIM_API_KEY from env

path = "./build/MyApp.app.tar.gz"
name = "my-app-v1.2.3.tar.gz"

with open(path, "rb") as f:
    data = f.read()
local_md5 = hashlib.md5(data).hexdigest()

asset = client.assets.get_or_create(name=name)
if asset.md5 != local_md5:
    r = requests.put(
        asset.signed_upload_url,
        data=data,
        headers={"Content-Type": "application/octet-stream"},
    )
    r.raise_for_status()

# asset.id, asset.name, asset.signed_download_url

For browser-direct uploads, generate the signedUploadUrl on your backend (where the API key lives) and hand only the URL to the browser. The browser PUTs to it without ever touching your LIM_API_KEY or proxying bytes through your servers. Useful when end users on your platform upload their own builds.

Install an asset

Two install paths:

Pre-install on boot

Reference your asset as an entry on the spec.initialAssets array during the provisioning call. This will start the simulator with the app already on the home screen.

Each entry must resolve to a binary the pod can fetch: either an existing Asset Storage entry (by name or ID), or a publicly reachable HTTPS URL such as a GitHub release asset, an S3 presigned URL, or anything else the cloud can reach. URLs on your LAN won't work.

# Local file: uploaded to Asset Storage on the fly, then installed at boot.
lim ios create --install ./MyApp.app.tar.gz

# Already in Asset Storage? Reference by name.
lim ios create --install-asset my-app-v1.2.3.tar.gz

# Repeat the flags for multiple separate apps.
lim ios create --install-asset a.tar.gz --install-asset b.tar.gz
const instance = await lim.iosInstances.create({
  wait: true,
  spec: {
    initialAssets: [
      // From Asset Storage by name
      { kind: 'App', source: 'AssetName', assetName: 'my-app-v1.2.3.tar.gz',
        launchMode: 'ForegroundIfRunning' },

      // From a public HTTPS URL
      { kind: 'App', source: 'URL', url: 'https://example.com/builds/123.tar.gz' },

      // By asset ID (when you already have one)
      { kind: 'App', source: 'AssetID', assetId: 'asset_01j...' },
    ],
  },
});

/**
 * launchMode controls behaviour after the install completes:
 *  - 'ForegroundIfRunning': bring to foreground if already running, otherwise launch
 *  - 'RelaunchIfRunning':   kill and relaunch if already running
 *  - undefined:             don't launch after installation
 */
inst, _ := lim.IosInstances.New(ctx, limrun.IosInstanceNewParams{
    Wait: param.NewOpt(true),
    Spec: limrun.IosInstanceNewParamsSpec{
        InitialAssets: []limrun.IosInstanceNewParamsSpecInitialAsset{
            {
                Kind:       "App",
                Source:     "AssetName",
                AssetName:  param.NewOpt("my-app-v1.2.3.tar.gz"),
                LaunchMode: "ForegroundIfRunning",
            },
            {
                Kind:   "App",
                Source: "URL",
                URL:    param.NewOpt("https://example.com/builds/123.tar.gz"),
            },
            {
                Kind:    "App",
                Source:  "AssetID",
                AssetID: param.NewOpt("asset_01j..."),
            },
        },
    },
})
instance = lim.ios_instances.create(
    wait=True,
    spec={
        "initial_assets": [
            {
                "kind": "App",
                "source": "AssetName",
                "asset_name": "my-app-v1.2.3.tar.gz",
                "launch_mode": "ForegroundIfRunning",
            },
            {
                "kind": "App",
                "source": "URL",
                "url": "https://example.com/builds/123.tar.gz",
            },
            {
                "kind": "App",
                "source": "AssetID",
                "asset_id": "asset_01j...",
            },
        ],
    },
)

IosInstanceCreateParams.Spec.InitialAsset shape:

FieldTypeMeaning
kind'App'iOS only supports App assets at boot. There is no 'Configuration' variant on iOS.
source'URL' | 'AssetName' | 'AssetID'How to resolve the asset.
url / assetName / assetIdstringThe matching identifier.
launchMode'ForegroundIfRunning' | 'RelaunchIfRunning'Launch behavior post-install. Omit to install without launching.
# Local APK uploaded to Asset Storage on the fly, then installed.
lim android create --install ./build.apk

# Multiple unrelated apps; each --install becomes its own install group.
lim android create --install ./app-a.apk --install ./app-b.apk

# Already in Asset Storage? Reference it by name instead.
lim android create --install-asset my-app-v1.2.3.apk
// Single APK
const instance = await lim.androidInstances.create({
  wait: true,
  spec: {
    initialAssets: [
      { kind: 'App', source: 'AssetName', assetName: 'my-app-v1.2.3.apk' },

      // From a public HTTPS URL
      { kind: 'App', source: 'URL', url: 'https://example.com/builds/123.apk' },

      // By asset ID
      { kind: 'App', source: 'AssetID', assetId: 'asset_01j...' },
    ],
  },
});

// Multiple separate apps: one initialAsset entry per app.
const multipleApps = await lim.androidInstances.create({
  wait: true,
  spec: {
    initialAssets: [
      { kind: 'App', source: 'AssetName', assetName: 'feature-a.apk' },
      { kind: 'App', source: 'AssetName', assetName: 'feature-b.apk' },
    ],
  },
});
inst, _ := lim.AndroidInstances.New(ctx, limrun.AndroidInstanceNewParams{
    Wait: param.NewOpt(true),
    Spec: limrun.AndroidInstanceNewParamsSpec{
        InitialAssets: []limrun.AndroidInstanceNewParamsSpecInitialAsset{
            {
                Kind:      "App",
                Source:    "AssetName",
                AssetName: param.NewOpt("my-app-v1.2.3.apk"),
            },
            {
                Kind:   "App",
                Source: "URL",
                URL:    param.NewOpt("https://example.com/builds/123.apk"),
            },
            {
                Kind:    "App",
                Source:  "AssetID",
                AssetID: param.NewOpt("asset_01j..."),
            },
        },
    },
})
instance = lim.android_instances.create(
    wait=True,
    spec={
        "initial_assets": [
            {"kind": "App", "source": "AssetName", "asset_name": "my-app-v1.2.3.apk"},
            {"kind": "App", "source": "URL", "url": "https://example.com/builds/123.apk"},
            {"kind": "App", "source": "AssetID", "asset_id": "asset_01j..."},
        ],
    },
)

Android's initialAsset has no launchMode field. Boot installs only. To launch an APK after the instance is ready, drive a launch from your control code (e.g. via ADB through startAdbTunnel).

If your app ships as a base APK plus density / locale / ABI splits (base.apk + config.*.apk), the whole set must install as one atomic group. See Split APKs for the grouped-install shape, which applies to both pre-install on boot and mid-session install.

AndroidInstanceCreateParams.Spec.InitialAsset shape:

FieldTypeMeaning
kind'App' | 'Configuration'App = APK install; Configuration = instance-level config value (Android-only; see Configuration entries on initialAssets).
source'URL' | 'URLs' | 'AssetName' | 'AssetNames' | 'AssetIDs'Singular variants install one APK. Plural variants (URLs, AssetNames, AssetIDs) install a split-APK group atomically: base APK first, config splits after.
url / assetName / assetIdstringSingular identifier.
urls / assetNames / assetIdsstring[]Split-APK group. The first entry is the base APK; the rest are density/locale/ABI splits.
configurationobjectUsed when kind: 'Configuration'.

For the rest of the create-call spec (regions, clues, timeouts, sandboxes), see Run an iOS simulator and Run an Android emulator.

Install on a running instance

When the user rebuilds while the embedded session is live, push the new asset and tell the running instance to install it. No restart, no reconnect.

The URL handed to the daemon has the same reachability rule as initialAssets: it must be a publicly reachable HTTPS URL (GitHub release asset, S3 presigned URL) or come from Asset Storage. The CLI's install-app accepts either a local path (uploaded to Asset Storage first, MD5-deduplicated) or a URL.

# Local path: uploaded to Asset Storage first, then installed.
lim ios install-app ./MyApp.app.tar.gz

# Remote URL: passed straight to the daemon.
lim ios install-app https://example.com/build.tar.gz
lim ios install-app https://... --md5 <hex> --launch-mode ForegroundIfRunning
// installApp accepts any URL the simulator can reach (signedDownloadUrl works).
// The md5 argument enables server-side caching: if the same bytes were
// installed on this region before, the install becomes near-instant.
await ios.installApp(asset.signedDownloadUrl, {
  md5: asset.md5,
  launchMode: 'ForegroundIfRunning',
});
_, err := iosClient.InstallApp(ctx, asset.SignedDownloadURL, &iosws.AppInstallationOptions{
    MD5:        asset.MD5,
    LaunchMode: "ForegroundIfRunning",
})

Python doesn't ship a WebSocket instance client. Run lim ios install-app from a subprocess, or call the WebSocket protocol directly.

installApp accepts an AppInstallationOptions object:

FieldTypeMeaning
md5stringMD5 hex digest. Enables server-side install caching for remote URLs.
timeoutMsnumberClient-side install timeout. Default 120_000 (120 s).
launchMode'ForegroundIfRunning' | 'RelaunchIfRunning'Launch behavior after install. Omit to install without launching.
# Local path: uploaded to Asset Storage first (MD5-deduplicated), then installed.
lim android install-app ./build.apk

# Remote URL: passed straight to the daemon.
lim android install-app https://example.com/build.apk --id <ID>
// Local file: upload once, then install via the returned signed URL.
const asset = await lim.assets.getOrUpload({ path: './build.apk' });
await android.sendAsset(asset.signedDownloadUrl);

// Or install straight from a public URL.
await android.sendAsset('https://example.com/build.apk');

// Default request timeout is 120 s. Pass a custom timeout for larger APKs.
await android.sendAsset(asset.signedDownloadUrl, 180_000);

sendAsset is TypeScript-only today. From Go or Python, drive the install via lim android install-app (or push the APK through the ADB tunnel; see ADB tunnel).

sendAsset(url, timeoutMs?) triggers a server-side download + pm install. The default client-side timeout is 120 s; pass a higher value if your APK is large enough that the install can run past two minutes.

Split APKs

If you ship your Android app as split APKs (a base.apk plus a few config.* APKs), the whole set must install as one atomic group or the app won't run.

Pass them together in one initialAsset (boot) or sendAsset (mid-session) call by giving the entry an array via the plural source variants (AssetNames, URLs, AssetIDs). The first filename is treated as the base APK; the rest are config splits. The server installs them via pm install-multiple.

// Pre-install at boot
await lim.androidInstances.create({
  wait: true,
  spec: {
    initialAssets: [
      // From Asset Storage by name
      { kind: 'App', source: 'AssetNames',
        assetNames: ['base.apk', 'config.xxhdpi.apk', 'config.en.apk'] },

      // Or from public HTTPS URLs the daemon can reach
      { kind: 'App', source: 'URLs',
        urls: [
          'https://example.com/base.apk',
          'https://example.com/config.xxhdpi.apk',
          'https://example.com/config.en.apk',
        ] },
    ],
  },
});
inst, _ := lim.AndroidInstances.New(ctx, limrun.AndroidInstanceNewParams{
    Wait: param.NewOpt(true),
    Spec: limrun.AndroidInstanceNewParamsSpec{
        InitialAssets: []limrun.AndroidInstanceNewParamsSpecInitialAsset{
            {
                Kind:       "App",
                Source:     "AssetNames",
                AssetNames: []string{"base.apk", "config.xxhdpi.apk", "config.en.apk"},
            },
            {
                Kind:   "App",
                Source: "URLs",
                URLs: []string{
                    "https://example.com/base.apk",
                    "https://example.com/config.xxhdpi.apk",
                    "https://example.com/config.en.apk",
                },
            },
        },
    },
})
lim.android_instances.create(
    wait=True,
    spec={
        "initial_assets": [
            {"kind": "App", "source": "AssetNames",
             "asset_names": ["base.apk", "config.xxhdpi.apk", "config.en.apk"]},

            {"kind": "App", "source": "URLs",
             "urls": [
                 "https://example.com/base.apk",
                 "https://example.com/config.xxhdpi.apk",
                 "https://example.com/config.en.apk",
             ]},
        ],
    },
)

Split APKs are SDK-only.

Use separate entries in initialAssets when you want to install unrelated apps in sequence; everything inside one entry is treated as a single grouped install.

Example response

A successful create returns the full instance resource. The fields you'll typically reach for from the embed and install paths:

{
  "metadata": {
    "id": "android_usw1_01krxvphn8e3rbeq0zewgw209a",
    "createdAt": "2026-05-19T19:52:42Z",
    "organizationId": "org_01kr52j...",
    "labels": { "tenant": "acme", "user": "user_42" }
  },
  "spec": {
    "inactivityTimeout": "10m0s",
    "region": "us-west"
  },
  "status": {
    "state": "ready",
    "apiUrl":                  "https://us-west-1.limrun.net/v1/android_usw1_.../api",
    "adbWebSocketUrl":         "wss://.../adbWebSocket",
    "endpointWebSocketUrl":    "wss://.../endpointWebSocket",
    "signedStreamUrl":         "https://console.limrun.com/signedStream?token=lim_...&url=…",
    "mcpUrl":                  "https://.../mcp",
    "targetHttpPortUrlPrefix": "wss://.../targetHttpPort",
    "token":                   "lim_..."
  }
}

state cycles through 'unknown' \| 'creating' \| 'assigned' \| 'ready' \| 'terminated'. spec.inactivityTimeout is returned as a Go duration string ("10m0s", not the "10m" you pass in).

The iOS shape is identical with these differences:

For the full field reference, see Status URL surface.

Working with Expo Go

Limrun maintains a small curated catalog of pre-built apps under the appstore/ name prefix. Currently, only Expo Go is available, which lets React Native users on your platform open a Snack or a local Expo project on a remote device without uploading anything.

Console Playground Apps picker showing Upload and App Store entries for Expo Go 55 and Expo Go 54

App Store asset reference:

Asset nameOSWhat it is
appstore/Expo-Go-54.0.6.tar.gziOSExpo Go 54.0.6 (iOS Simulator build)
appstore/Expo-Go-55-iOS-latest.tar.gziOSExpo Go 55, latest (iOS Simulator build)
appstore/Expo-Go-54.0.6.apkAndroidExpo Go 54.0.6 (APK)
appstore/Expo-Go-55-latest.apkAndroidExpo Go 55, latest (APK)

Install at boot the same way as one of your own assets. The appstore/ prefix is just part of the name:

spec: {
  initialAssets: [{
    kind: 'App',
    source: 'AssetName',
    assetName: 'appstore/Expo-Go-55-iOS-latest.tar.gz',   // or .apk for Android
    launchMode: 'ForegroundIfRunning',
  }],
}

Pair the boot install with the <RemoteControl openUrl="exp://…" /> prop to land the device on the user's Expo project automatically on first paint:

<RemoteControl
  url={session.webrtcUrl}
  token={session.token}
  openUrl="exp://exp.host/@user/my-snack"
/>

That's the full integration: backend creates the instance with the Expo Go asset, frontend renders <RemoteControl /> with the project URL. No Expo Go upload, no build pipeline, no app-store dance.

List the available App Store entries to discover what else is curated:

lim asset list --include-app-store
const apps = await lim.assets.list({
  includeAppStore: true,
  namePrefixFilter: 'appstore/',
});

The appstore/ prefix is part of the stored name on the client side, but the API strips it before querying the App Store catalog server-side. That means nameFilter: 'appstore/Expo-Go-54.0.6.tar.gz' works as expected when combined with includeAppStore: true, but a partial prefix like nameFilter: 'appstor' will never match anything in the App Store, since the comparison happens after the prefix is removed.

Asset organisation for multi-tenancy

Asset names are global to your org, not per-tenant. The store has no built-in tenant scoping, no labels, no folders. The single primitive you have is the name itself, plus namePrefixFilter to query it.

Use prefixes to divide namespaces

The convention is to put the tenant (or workspace, customer, project, GitHub org/repo, PR number, anything stable) at the front of the name as a prefix:

acme/my-app-v1.2.3.tar.gz
acme/my-app-2026-05-18-abc1234.tar.gz
acme/my-app-pr-742.apk
globex/expo-snack-build.tar.gz

The slash isn't required, but it reads well and survives any prefix scan. Pick a separator and stick to it across your platform.

Two patterns fall out of this naturally:

// List one tenant's assets (for an admin UI or a usage dashboard)
const acmeAssets = await lim.assets.list({
  namePrefixFilter:   'acme/',
  includeDownloadUrl: true,
});

// Offboarding: drop everything a tenant ever uploaded
const stale = await lim.assets.list({ namePrefixFilter: 'acme/' });
for (const a of stale) {
  await lim.assets.delete(a.id);
}

The os field on each Asset lets you further split a tenant's namespace by platform when the same logical name covers both an iOS sim build and an Android APK:

const list = await lim.assets.list({ namePrefixFilter: 'acme/' });
const iosOnly     = list.filter((a) => a.os === 'ios');
const androidOnly = list.filter((a) => a.os === 'android');
const universal   = list.filter((a) => !a.os);

Filtering options

assets.list, assets.get, and assets.delete are the operations you'll wire into your admin UI, your reaper, and your offboarding flow.

lim asset list
lim asset list --name my-app-v1.2.3.tar.gz --download-url
lim asset list <id> --download-url
lim asset delete <id>
const assets = await lim.assets.list({
  limit: 50,
  nameFilter: 'my-app-v1.2.3.tar.gz',         // exact match
  // namePrefixFilter: 'acme/',                // alternative; LIKE wildcards treated as literals
  includeDownloadUrl: true,
  includeUploadUrl:   false,
  includeAppStore:    false,
});

const single = await lim.assets.get(assetId, { includeDownloadUrl: true });

await lim.assets.delete(assetId);
assets, err := lim.Assets.List(ctx, limrun.AssetListParams{
    Limit:              param.NewOpt(int64(50)),
    NameFilter:         param.NewOpt("my-app-v1.2.3.tar.gz"),
    IncludeDownloadURL: param.NewOpt(true),
})

single, err := lim.Assets.Get(ctx, assetID, limrun.AssetGetParams{
    IncludeDownloadURL: param.NewOpt(true),
})

err = lim.Assets.Delete(ctx, assetID)
from limrun_api import Limrun

client = Limrun()

assets = client.assets.list(
    limit=50,
    name_filter="my-app-v1.2.3.tar.gz",
    include_download_url=True,
)

single = client.assets.get(asset_id, include_download_url=True)

client.assets.delete(asset_id)

Full set of filters on assets.list:

FilterBehavior
nameFilterCase-sensitive exact match on name. Cannot combine with namePrefixFilter.
namePrefixFilterCase-sensitive prefix match. LIKE wildcards (%, _) are treated as literal characters. Empty string is rejected with 400, so omit the param entirely if you don't want filtering. Cannot combine with nameFilter.
includeDownloadUrl / includeUploadUrlSet to true to have signed URLs included on each returned asset. Default is false (smaller response).
includeAppStoreInclude curated assets from the Limrun App Store, returned with the appstore/ name prefix.
limitMaximum number of items returned. Default 50.

The CLI maps these to --name, --download-url, --upload-url, and --include-app-store (no --name-prefix, use SDK for this).

Next steps

monitor-play

Embed the simulator

Render <RemoteControl /> with initialAssets so the embedded device opens straight into the customer's app.

hammer

Build with remote Xcode

lim xcode build --upload is the canonical way to land an iOS simulator build into Asset Storage.

git-pull-request

Automatic PR Previews

Wire the upload step into a GitHub workflow that posts a preview link on every PR.

book-open

SDK Reference

assets.list / getOrCreate / getOrUpload parameters across TypeScript, Python, and Go.