Skip to content

App & Machine Management

Low-level Fly.io app and machine lifecycle operations.

ensure_app async

ensure_app(app_name=None, *, org=DEFAULT_ORG, region=DEFAULT_REGION, token=None)

Ensure a Fly.io app exists, creating it if necessary.

Parameters:

Name Type Description Default
app_name str | None

Name for the Fly app. Defaults to flaude.

None
org str

Fly.io organization slug. Defaults to personal.

DEFAULT_ORG
region str

Preferred Fly.io region for machines created under this app. Defaults to iad. Only applied when a new app is created.

DEFAULT_REGION
token str | None

Optional explicit API token (otherwise reads FLY_API_TOKEN).

None

Returns:

Type Description
FlyApp

A FlyApp dataclass with the app name, org, and preferred region.

Source code in flaude/app.py
async def ensure_app(
    app_name: str | None = None,
    *,
    org: str = DEFAULT_ORG,
    region: str = DEFAULT_REGION,
    token: str | None = None,
) -> FlyApp:
    """Ensure a Fly.io app exists, creating it if necessary.

    Args:
        app_name: Name for the Fly app. Defaults to ``flaude``.
        org: Fly.io organization slug. Defaults to ``personal``.
        region: Preferred Fly.io region for machines created under this app.
            Defaults to ``iad``.  Only applied when a new app is created.
        token: Optional explicit API token (otherwise reads FLY_API_TOKEN).

    Returns:
        A FlyApp dataclass with the app name, org, and preferred region.
    """
    name = app_name or DEFAULT_APP_PREFIX

    existing = await get_app(name, token=token)
    if existing is not None:
        logger.info("Fly app %r already exists, reusing", name)
        # Preserve the caller's region preference even for existing apps
        if existing.region != region:
            return FlyApp(name=existing.name, org=existing.org, region=region)
        return existing

    return await create_app(name, org=org, region=region, token=token)

create_app async

create_app(app_name, *, org=DEFAULT_ORG, region=DEFAULT_REGION, token=None)

Create a new Fly.io app with configurable name, org, and region.

The region is stored in the returned :class:FlyApp as the preferred region for machines created under this app. Fly.io assigns machines to regions at machine-creation time, not at app-creation time, so this value is used as a convenient default rather than sent to the app-creation API.

Parameters:

Name Type Description Default
app_name str

Unique name for the new Fly.io application.

required
org str

Fly.io organization slug that will own the app. Defaults to personal.

DEFAULT_ORG
region str

Preferred Fly.io region for machines in this app (e.g. iad, lax, fra). Defaults to iad.

DEFAULT_REGION
token str | None

Optional explicit API token (otherwise reads FLY_API_TOKEN).

None

Returns:

Name Type Description
A FlyApp

class:FlyApp dataclass with the app name, org, and region.

Raises:

Type Description
FlyAPIError

If the API call fails (e.g. name taken, auth error).

Source code in flaude/app.py
async def create_app(
    app_name: str,
    *,
    org: str = DEFAULT_ORG,
    region: str = DEFAULT_REGION,
    token: str | None = None,
) -> FlyApp:
    """Create a new Fly.io app with configurable name, org, and region.

    The ``region`` is stored in the returned :class:`FlyApp` as the preferred
    region for machines created under this app.  Fly.io assigns machines to
    regions at machine-creation time, not at app-creation time, so this value
    is used as a convenient default rather than sent to the app-creation API.

    Args:
        app_name: Unique name for the new Fly.io application.
        org: Fly.io organization slug that will own the app.
            Defaults to ``personal``.
        region: Preferred Fly.io region for machines in this app
            (e.g. ``iad``, ``lax``, ``fra``).  Defaults to ``iad``.
        token: Optional explicit API token (otherwise reads ``FLY_API_TOKEN``).

    Returns:
        A :class:`FlyApp` dataclass with the app name, org, and region.

    Raises:
        FlyAPIError: If the API call fails (e.g. name taken, auth error).
    """
    payload = {
        "app_name": app_name,
        "org_slug": org,
    }
    logger.info(
        "Creating Fly app %r in org %r (preferred region: %s)", app_name, org, region
    )
    await fly_post("/apps", json=payload, token=token)
    logger.info("Fly app %r created successfully", app_name)
    return FlyApp(name=app_name, org=org, region=region)

get_app async

get_app(app_name, *, token=None)

Return a FlyApp if it already exists, or None if not found.

Parameters:

Name Type Description Default
app_name str

The Fly.io application name to look up.

required
token str | None

Optional explicit API token (otherwise reads FLY_API_TOKEN).

None

Returns:

Name Type Description
A FlyApp | None

class:FlyApp dataclass if the app exists, or None if the app

FlyApp | None

is not found (HTTP 404).

Raises:

Type Description
FlyAPIError

If the API returns any error other than 404.

Source code in flaude/app.py
async def get_app(app_name: str, *, token: str | None = None) -> FlyApp | None:
    """Return a FlyApp if it already exists, or None if not found.

    Args:
        app_name: The Fly.io application name to look up.
        token: Optional explicit API token (otherwise reads ``FLY_API_TOKEN``).

    Returns:
        A :class:`FlyApp` dataclass if the app exists, or ``None`` if the app
        is not found (HTTP 404).

    Raises:
        FlyAPIError: If the API returns any error other than 404.
    """
    try:
        data = await fly_get(f"/apps/{app_name}", token=token)
        if data and isinstance(data, dict):
            return FlyApp(
                name=data.get("name", app_name),
                org=data.get("organization", {}).get("slug", DEFAULT_ORG)
                if isinstance(data.get("organization"), dict)
                else DEFAULT_ORG,
            )
        return None
    except FlyAPIError as exc:
        if exc.status_code == 404:
            return None
        raise

FlyApp dataclass

FlyApp(name, org, region=DEFAULT_REGION)

Represents a Fly.io application used by flaude.

Attributes:

Name Type Description
name str

The Fly.io application name.

org str

The Fly.io organization slug that owns the app.

region str

The preferred region for machines created under this app. Stored locally; not a Fly.io API-level concept for apps.

create_machine async

create_machine(app_name, config, *, name=None, token=None, timeout=60.0)

Create a Fly.io machine and return its ID/status.

Sends a POST to /v1/apps/{app}/machines with the payload built from config. The Fly API returns the machine details synchronously once the machine has been accepted (not necessarily started).

Parameters:

Name Type Description Default
app_name str

The Fly app to create the machine under.

required
config MachineConfig

A :class:MachineConfig describing the desired machine.

required
name str | None

Optional human-readable name for the machine.

None
token str | None

Explicit API token (falls back to FLY_API_TOKEN).

None
timeout float

HTTP request timeout in seconds.

60.0

Returns:

Name Type Description
A FlyMachine

class:FlyMachine with the machine's ID, state, region, etc.

Raises:

Type Description
ValueError

If required config fields are missing.

FlyAPIError

If the Fly API returns an error.

Source code in flaude/machine.py
async def create_machine(
    app_name: str,
    config: MachineConfig,
    *,
    name: str | None = None,
    token: str | None = None,
    timeout: float = 60.0,
) -> FlyMachine:
    """Create a Fly.io machine and return its ID/status.

    Sends a POST to ``/v1/apps/{app}/machines`` with the payload built from
    *config*.  The Fly API returns the machine details synchronously once the
    machine has been accepted (not necessarily started).

    Args:
        app_name: The Fly app to create the machine under.
        config: A :class:`MachineConfig` describing the desired machine.
        name: Optional human-readable name for the machine.
        token: Explicit API token (falls back to ``FLY_API_TOKEN``).
        timeout: HTTP request timeout in seconds.

    Returns:
        A :class:`FlyMachine` with the machine's ID, state, region, etc.

    Raises:
        ValueError: If required config fields are missing.
        FlyAPIError: If the Fly API returns an error.
    """
    payload = build_machine_config(config)

    if name:
        payload["name"] = name

    logger.info(
        "Creating machine in app %r region=%s image=%s",
        app_name,
        config.region,
        config.image,
    )

    data = await fly_post(
        f"/apps/{app_name}/machines",
        json=payload,
        token=token,
        timeout=timeout,
    )

    if not data or not isinstance(data, dict):
        raise FlyAPIError(
            status_code=0,
            detail="Empty or invalid response from create-machine endpoint",
            method="POST",
            url=f"/apps/{app_name}/machines",
        )

    machine = _parse_machine_response(data, app_name)
    logger.info(
        "Machine %s created (state=%s, region=%s)",
        machine.id,
        machine.state,
        machine.region,
    )
    return machine

get_machine async

get_machine(app_name, machine_id, *, token=None)

Fetch the current state of a machine.

Parameters:

Name Type Description Default
app_name str

The Fly app the machine belongs to.

required
machine_id str

The machine ID.

required
token str | None

Explicit API token.

None

Returns:

Name Type Description
A FlyMachine

class:FlyMachine with updated state.

Source code in flaude/machine.py
async def get_machine(
    app_name: str,
    machine_id: str,
    *,
    token: str | None = None,
) -> FlyMachine:
    """Fetch the current state of a machine.

    Args:
        app_name: The Fly app the machine belongs to.
        machine_id: The machine ID.
        token: Explicit API token.

    Returns:
        A :class:`FlyMachine` with updated state.
    """
    data = await fly_get(
        f"/apps/{app_name}/machines/{machine_id}",
        token=token,
    )
    if not data or not isinstance(data, dict):
        raise FlyAPIError(
            status_code=0,
            detail="Empty or invalid response from get-machine endpoint",
            method="GET",
            url=f"/apps/{app_name}/machines/{machine_id}",
        )
    return _parse_machine_response(data, app_name)

stop_machine async

stop_machine(app_name, machine_id, *, token=None)

Send a stop signal to a machine.

This is a best-effort call — if the machine is already stopped or destroyed the error is suppressed.

Parameters:

Name Type Description Default
app_name str

The Fly app the machine belongs to.

required
machine_id str

The machine ID to stop.

required
token str | None

Explicit API token (falls back to FLY_API_TOKEN).

None
Source code in flaude/machine.py
async def stop_machine(
    app_name: str,
    machine_id: str,
    *,
    token: str | None = None,
) -> None:
    """Send a stop signal to a machine.

    This is a best-effort call — if the machine is already stopped or
    destroyed the error is suppressed.

    Args:
        app_name: The Fly app the machine belongs to.
        machine_id: The machine ID to stop.
        token: Explicit API token (falls back to ``FLY_API_TOKEN``).
    """
    try:
        await fly_post(
            f"/apps/{app_name}/machines/{machine_id}/stop",
            token=token,
        )
        logger.info("Stop signal sent to machine %s", machine_id)
    except FlyAPIError as exc:
        # 404 = already gone, 409 = already stopped / not in stoppable state
        if exc.status_code in (404, 409):
            logger.debug(
                "Machine %s stop returned %s (already stopped/gone)",
                machine_id,
                exc.status_code,
            )
        else:
            raise

destroy_machine async

destroy_machine(app_name, machine_id, *, force=True, token=None)

Destroy a machine, removing it permanently.

Parameters:

Name Type Description Default
app_name str

The Fly app the machine belongs to.

required
machine_id str

The machine ID to destroy.

required
force bool

If True, force-destroy even if the machine is running.

True
token str | None

Explicit API token.

None
Source code in flaude/machine.py
async def destroy_machine(
    app_name: str,
    machine_id: str,
    *,
    force: bool = True,
    token: str | None = None,
) -> None:
    """Destroy a machine, removing it permanently.

    Args:
        app_name: The Fly app the machine belongs to.
        machine_id: The machine ID to destroy.
        force: If True, force-destroy even if the machine is running.
        token: Explicit API token.
    """
    path = f"/apps/{app_name}/machines/{machine_id}"
    if force:
        path += "?force=true"

    try:
        await fly_delete(path, token=token)
        logger.info("Machine %s destroyed", machine_id)
    except FlyAPIError as exc:
        if exc.status_code == 404:
            logger.debug("Machine %s already gone (404)", machine_id)
        else:
            raise

start_machine async

start_machine(app_name, machine_id, *, token=None)

Start a stopped machine.

Best-effort — if the machine is already started or gone, the error is suppressed.

Parameters:

Name Type Description Default
app_name str

The Fly app the machine belongs to.

required
machine_id str

The machine ID to start.

required
token str | None

Explicit API token (falls back to FLY_API_TOKEN).

None
Source code in flaude/machine.py
async def start_machine(
    app_name: str,
    machine_id: str,
    *,
    token: str | None = None,
) -> None:
    """Start a stopped machine.

    Best-effort — if the machine is already started or gone, the error
    is suppressed.

    Args:
        app_name: The Fly app the machine belongs to.
        machine_id: The machine ID to start.
        token: Explicit API token (falls back to ``FLY_API_TOKEN``).
    """
    try:
        await fly_post(
            f"/apps/{app_name}/machines/{machine_id}/start",
            token=token,
        )
        logger.info("Start signal sent to machine %s", machine_id)
    except FlyAPIError as exc:
        if exc.status_code in (404, 409):
            logger.debug(
                "Machine %s start returned %s (already started/gone)",
                machine_id,
                exc.status_code,
            )
        else:
            raise

update_machine async

update_machine(app_name, machine_id, config, *, name=None, token=None, timeout=60.0)

Update a stopped machine's configuration.

Sends a PUT to /v1/apps/{app}/machines/{id} with the full config payload. Used to inject new env vars (prompt, session ID) between session turns.

Parameters:

Name Type Description Default
app_name str

The Fly app the machine belongs to.

required
machine_id str

The machine ID to update.

required
config MachineConfig

Updated :class:MachineConfig.

required
name str | None

Optional machine name override.

None
token str | None

Explicit API token.

None
timeout float

HTTP request timeout in seconds.

60.0

Returns:

Name Type Description
A FlyMachine

class:FlyMachine with updated state.

Source code in flaude/machine.py
async def update_machine(
    app_name: str,
    machine_id: str,
    config: MachineConfig,
    *,
    name: str | None = None,
    token: str | None = None,
    timeout: float = 60.0,
) -> FlyMachine:
    """Update a stopped machine's configuration.

    Sends a PUT to ``/v1/apps/{app}/machines/{id}`` with the full config
    payload. Used to inject new env vars (prompt, session ID) between
    session turns.

    Args:
        app_name: The Fly app the machine belongs to.
        machine_id: The machine ID to update.
        config: Updated :class:`MachineConfig`.
        name: Optional machine name override.
        token: Explicit API token.
        timeout: HTTP request timeout in seconds.

    Returns:
        A :class:`FlyMachine` with updated state.
    """
    payload = build_machine_config(config)
    if name:
        payload["name"] = name

    logger.info("Updating machine %s in app %r", machine_id, app_name)

    data = await fly_put(
        f"/apps/{app_name}/machines/{machine_id}",
        json=payload,
        token=token,
        timeout=timeout,
    )

    if not data or not isinstance(data, dict):
        raise FlyAPIError(
            status_code=0,
            detail="Empty or invalid response from update-machine endpoint",
            method="PUT",
            url=f"/apps/{app_name}/machines/{machine_id}",
        )

    machine = _parse_machine_response(data, app_name)
    logger.info("Machine %s updated (state=%s)", machine.id, machine.state)
    return machine

FlyMachine dataclass

FlyMachine(id, name, state, region, instance_id, app_name)

Represents a running (or recently created) Fly.io machine.

Attributes:

Name Type Description
id str

The unique Fly machine ID assigned by the Fly Machines API.

name str

Human-readable name for the machine (may be empty).

state str

Current machine state (e.g. created, started, stopped).

region str

The Fly.io region the machine is running in (e.g. iad).

instance_id str

Internal instance identifier assigned by Fly.io.

app_name str

The Fly.io application this machine belongs to.

cleanup async

cleanup(*, token=None)

Stop and destroy this machine, handling all edge cases gracefully.

This method first attempts to stop the machine, then destroys it. Both steps tolerate already-stopped and already-destroyed states, ensuring no orphaned resources remain.

Parameters:

Name Type Description Default
token str | None

Explicit API token (falls back to FLY_API_TOKEN).

None
Source code in flaude/machine.py
async def cleanup(self, *, token: str | None = None) -> None:
    """Stop and destroy this machine, handling all edge cases gracefully.

    This method first attempts to stop the machine, then destroys it.
    Both steps tolerate already-stopped and already-destroyed states,
    ensuring no orphaned resources remain.

    Args:
        token: Explicit API token (falls back to ``FLY_API_TOKEN``).
    """
    logger.info("Cleaning up machine %s in app %s", self.id, self.app_name)
    await stop_machine(self.app_name, self.id, token=token)
    await destroy_machine(self.app_name, self.id, token=token)