---
title: Runtime and Lifecycle
description: Declare runtime functions, schedules, event handlers, lifecycle checks, and startup activation work.
---

Runtime work is host-owned. A module declares functions, schedules, event
handlers, and lifecycle needs; the worker claims queues, applies retry policy,
records runs, and projects bounded records into Lenso Console.

## Function declaration

Declare runtime functions in the manifest:

```rust
use lenso::{ModuleManifest, RuntimeFunctionDeclaration, RuntimeSurface};

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support/tickets")
        .runtime(RuntimeSurface {
            functions: vec![RuntimeFunctionDeclaration {
                name: "support.ticket.escalate.v1".to_owned(),
                version: 1,
                queue: "support".to_owned(),
                input_schema: Some("support.ticket.escalate.v1".to_owned()),
                retry_policy: None,
                operation: None,
            }],
            schedules: vec![],
            workflows: vec![],
        })
        .build()
}
```

The declaration makes the function visible and lintable. Linked Modules
register executable handlers through their binding. A Service owns the provider
API used to execute operations for the Modules it declares.

## Scheduled runtime functions

Declare schedules beside the functions they enqueue:

```rust
use lenso::{
    ModuleManifest, RuntimeFunctionDeclaration, RuntimeSurface,
    ScheduledFunctionDeclaration,
};

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support/tickets")
        .runtime(RuntimeSurface {
            functions: vec![RuntimeFunctionDeclaration {
                name: "support.ticket.escalate_overdue.v1".to_owned(),
                version: 1,
                queue: "support".to_owned(),
                input_schema: Some("support.ticket.escalate_overdue.v1".to_owned()),
                retry_policy: None,
                operation: None,
            }],
            schedules: vec![ScheduledFunctionDeclaration {
                name: "escalate-overdue-tickets".to_owned(),
                function_name: "support.ticket.escalate_overdue.v1".to_owned(),
                cron: "*/15 * * * *".to_owned(),
                input: serde_json::json!({ "source": "cron" }),
            }],
            workflows: vec![],
        })
        .build()
}
```

`cron` uses standard 5-field UTC cron:

```text
minute hour day-of-month month day-of-week
```

Supported field forms are `*`, comma lists, ranges, step values such as
`*/15`, numbers, month names such as `JAN`, and weekday names such as `MON`.
Seconds, time zones, and Quartz-style fields are intentionally not part of the
module contract.

The scheduler stores durable state in `runtime.scheduled_functions`. When a
schedule is due, the worker enqueues the declared runtime function into
`runtime.function_runs`; retries, dead letters, logs, and Runtime Story records
then follow the same path as manually enqueued runtime work. Provider execution
does not transfer ownership of the Host queue or scheduling state.

## Runtime Story records

The worker writes durable records:

| Table/source | Meaning |
| --- | --- |
| `platform.outbox` | module event dispatch state |
| `runtime.function_runs` | queued, running, retried, completed, or failed function work |
| `platform.story_events` | story projection records |
| execution logs | technical operations and provider runtime metadata |

Lenso Console uses those records to show Stories, function operations, dead
letters, queues, retries, and technical operations.

See [Runtime Stories](/docs/runtime-stories) for the correlation model, story
graph shape, and module metadata that controls story titles.

## Lifecycle declarations

Lifecycle is data, not an arbitrary startup hook:

```rust
use lenso::{
    LifecycleActivationJobDeclaration, LifecycleActivationRunPolicy,
    LifecycleStartupCheckDeclaration, LifecycleStartupCheckKind, LifecycleSurface,
    ModuleManifest, RuntimeFunctionDeclaration, RuntimeSurface,
};

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support/tickets")
        .runtime(RuntimeSurface {
            functions: vec![RuntimeFunctionDeclaration {
                name: "support.ticket.reindex.v1".to_owned(),
                version: 1,
                queue: "support".to_owned(),
                input_schema: Some("support.ticket.reindex.v1".to_owned()),
                retry_policy: None,
                operation: None,
            }],
            schedules: vec![],
            workflows: vec![],
        })
        .lifecycle(LifecycleSurface {
            startup_checks: vec![LifecycleStartupCheckDeclaration {
                name: "reindex function is registered".to_owned(),
                required: true,
                check: LifecycleStartupCheckKind::FunctionRegistered {
                    function_name: "support.ticket.reindex.v1".to_owned(),
                },
            }],
            activation_jobs: vec![LifecycleActivationJobDeclaration {
                name: "warm ticket index".to_owned(),
                function_name: "support.ticket.reindex.v1".to_owned(),
                run_policy: LifecycleActivationRunPolicy::EveryStartup,
                input: serde_json::json!({ "reason": "startup" }),
                required: false,
            }],
        })
        .build()
}
```

`required: true` lifecycle failures block startup. `required: false` failures
warn and continue. Activation jobs currently use `every_startup`; non-idempotent
install or upgrade lifecycle state is intentionally deferred.

## Service event-handler boundary

Service event handlers use the Host-owned outbox relay:

1. module command writes business tables
2. same transaction inserts `platform.outbox`
3. worker claims pending outbox rows
4. Host invokes a Linked handler or Service provider operation
5. handler may return a bounded `enqueue_function` action
6. host inserts `runtime.function_runs`
7. retries and dead letters stay in host tables

Services never claim Host outbox rows directly.

## Service runtime boundary

For provider-backed functions, the Worker remains the Host queue owner. The
Service owns its API, Workload, and terminal execution records. Correlation,
actor, trace, and retry identities cross the provider contract explicitly; no
distributed transaction or second Module source is introduced.

## Definition of done

A runtime-capable module is done when:

- function declarations lint cleanly
- schedule declarations reference existing functions and valid cron expressions
- linked or remote executors are registered
- lifecycle checks reference existing functions or capabilities
- a smoke path creates a function run
- Lenso Console shows the run, retry, failure, or Story records
