Skip to content
Lenso
English
Esc
navigateopen⌘Jpreview
On this page

Runtime and Lifecycle

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 evidence into Runtime Console.

Function declaration

Declare runtime functions in the manifest:

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

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support")
        .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,
            }],
            schedules: 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:

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

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support")
        .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,
            }],
            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" }),
            }],
        })
        .build()
}

cron uses standard 5-field UTC cron:

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 evidence 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 evidence

The worker writes durable evidence:

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

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

See 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:

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

pub fn manifest() -> ModuleManifest {
    ModuleManifest::builder("support")
        .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,
            }],
            schedules: 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 evidence. 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
  • Runtime Console shows the run, retry, failure, or story evidence

Last updated on August 1, 2026

Was this page helpful?