This chapter is a fork in the road, not a fourteenth step. Take it when your service runs on plain Axum, not on autumn-web. It runs the Chapter 2 workflow with no HarvestPlugin and no AppBuilder.

Workflows, activities, timers, signals and child workflows are the same on both paths. Three things change: how the process starts the engine, how it serves the management API, and how the migrations run.

embedding.md is the reference for this path. This chapter is the short version.

Prerequisites

  • Read Chapter 2 first. It explains the workflow and the activity. Skip its HarvestPlugin registration.
  • A clone of this repository. The code in this chapter is the crate examples/standalone-quickstart, and CI runs each command below as written.
  • Docker, for Postgres.
  • jq.

Run each command from the repository root. HarvestEmbedding ships in 0.7.0 and later.

1. The crate

TOML
# This crate inherits no workspace settings, so you can copy it out of the
# repository as it is.
[package]
name = "standalone-quickstart"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
autumn-harvest = { version = "0.10.0", path = "../../autumn-harvest" }
autumn-harvest-plugin = { version = "0.10.0", path = "../../autumn-harvest-plugin" }
autumn-web = "0.10"
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
tracing = "0.1"
tracing-subscriber = "0.3"

[dev-dependencies]
autumn-harvest = { version = "0.10.0", path = "../../autumn-harvest", features = ["testing"] }

The crate does not call autumn-web's AppBuilder. It still depends on autumn-web. autumn-harvest-plugin needs it, and main.rs uses its Axum re-export. Issue #1615 tracks the removal of that dependency.

Outside this repository, remove each path key. Set each version to the first release that contains HarvestEmbedding.

2. The workflow

src/workflows.rs holds the Chapter 2 code with no change. Below it, harvest_builder registers the workflow and the activity.

Rust
use std::time::Duration;

use autumn_harvest::prelude::*;

#[workflow]
async fn onboarding(ctx: &WorkflowContext, user_id: i64) -> HarvestResult<String> {
    let result = ctx
        .execute_activity_raw(
            "send_welcome_email",
            serde_json::json!({ "user_id": user_id }),
            "default",
        )
        .await?;

    Ok(result["status"].as_str().unwrap_or("sent").to_owned())
}

#[activity(start_to_close = "30s", retry = RetryPolicy::exponential(3, Duration::from_secs(1)))]
async fn send_welcome_email(
    _ctx: &ActivityContext,
    input: serde_json::Value,
) -> HarvestResult<serde_json::Value> {
    let user_id = input["user_id"].as_i64().unwrap_or_default();
    tracing::info!(user_id, "sending welcome email");
    Ok(serde_json::json!({ "status": "sent" }))
}

/// Register the workflow and the activity. On the plugin path,
/// `HarvestPlugin::workflows` and `HarvestPlugin::activities` do this.
pub fn harvest_builder() -> HarvestBuilder {
    HarvestBuilder::default()
        .workflows(workflows![onboarding])
        .activities(activities![send_welcome_email])
        .worker(WorkerConfig::default())
}

#[cfg(test)]
mod tests;

The tests module runs onboarding under WorkflowSimulator, with the activity mocked. Chapter 11 covers workflow tests.

3. The server

Rust
//! The Chapter 2 workflow on a plain Axum server, with no `HarvestPlugin`.

mod workflows;

use autumn_harvest::diesel_async::AsyncPgConnection;
use autumn_harvest::diesel_async::pooled_connection::AsyncDieselConnectionManager;
use autumn_harvest::diesel_async::pooled_connection::deadpool::Pool;
use autumn_harvest_plugin::prelude::*;
use autumn_web::reexports::axum;

type BoxError = Box<dyn std::error::Error + Send + Sync>;

#[tokio::main]
async fn main() -> Result<(), BoxError> {
    // Print the engine's log lines, for example the dev-profile warning.
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    let database_url = std::env::var("DATABASE_URL").map_err(|_| "set DATABASE_URL")?;
    let manager = AsyncDieselConnectionManager::<AsyncPgConnection>::new(&database_url);
    let pool = Pool::builder(manager).max_size(10).build()?;

    // The default config runs the worker and the scheduler in this process.
    // The outbox relay needs autumn-web, so `HarvestEmbedding` does not run it.
    let config = HarvestRuntimeConfig {
        database: HarvestDatabaseConfig {
            url: Some(database_url),
        },
        outbox: HarvestOutboxConfig {
            enabled: false,
            ..HarvestOutboxConfig::default()
        },
        ..HarvestRuntimeConfig::default()
    };

    // Bind first. A busy port then stops the process before a worker starts.
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;

    // Run the startup sequence of `HarvestPlugin` without autumn-web's `AppBuilder`.
    let harvest = HarvestEmbedding::new(
        workflows::harvest_builder().try_build()?,
        config,
        HarvestRunnerResources::new(pool),
    )
    .with_ambient_profile()
    .start()
    .await
    .map_err(|error| format!("Harvest did not start: {error}"))?;

    let app = axum::Router::new().nest("/api/harvest", harvest.router());
    tracing::info!("listening on http://127.0.0.1:3000");
    let served = axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await;

    // Drain the worker and remove the process globals, also after a serve error.
    harvest.stop().await;
    tracing::info!("Harvest stopped");
    served?;
    Ok(())
}

/// Wait for Ctrl-C.
async fn shutdown_signal() {
    if let Err(error) = tokio::signal::ctrl_c().await {
        tracing::warn!(%error, "cannot listen for Ctrl-C");
    }
}

HarvestEmbedding::start runs the same startup steps as HarvestPlugin. Start the runtime lists them.

harvest.router() is a plain axum::Router. Nest it under any path. harvest.stop() drains the worker after the server stops.

4. Run it

Start Postgres:

Yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: harvest
      POSTGRES_PASSWORD: harvest
      POSTGRES_DB: harvest
    ports:
      - "127.0.0.1:5435:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U harvest"]
      interval: 2s
      timeout: 5s
      retries: 15
Shell
docker compose -f examples/standalone-quickstart/compose.yaml up -d --wait

Apply the Harvest migrations. On the plugin path, the dev profile applies them at boot. Here, the harvest CLI applies them:

Shell
cargo run -p autumn-harvest-cli -- migrate run \
  --database-url postgres://harvest:harvest@localhost:5435/harvest

Start the server:

Shell
DATABASE_URL=postgres://harvest:harvest@localhost:5435/harvest \
AUTUMN_PROFILE=dev \
cargo run -p standalone-quickstart

with_ambient_profile() reads AUTUMN_ENV, then AUTUMN_PROFILE. The dev profile opens the admin routes to any caller with no credential. The server logs a warning about this.

Other profiles refuse the admin routes and every mutating route to a caller with no credential. Read routes stay open when you add no auth layer of your own. So keep this server on 127.0.0.1.

5. Start the workflow

In a second terminal, send the Chapter 2 request:

Shell
curl -s -X POST http://localhost:3000/api/harvest/workflows/onboarding/start \
  -H 'Content-Type: application/json' \
  -d '{"workflow_id":"user-42","input":42}' | jq .

Wait for the result. The request returns when the run completes, or after 30 seconds:

Shell
curl -s 'http://localhost:3000/api/harvest/workflows/by-id/onboarding/user-42/result?wait=30s' | jq .

Open http://localhost:3000/api/harvest/ui/workflows to see the run in the Vantage dashboard.

6. Run preflight

Shell
cargo run -p autumn-harvest-cli -- preflight

The CLI's default base URL is http://localhost:3000/api/harvest. Preflight exits 0 on a pass and 2 on a warning. Expect 0 or 2 here. The admin_auth_boundary row passes, and it says that any caller can reach the admin API.

Press Ctrl-C in the server terminal. The server stops, drains the worker, and logs Harvest stopped.

Before production

The dev profile is for this chapter only. Read these sections of the reference before you deploy:

Back to the main path

Continue at Chapter 3. Add each workflow and activity to src/workflows.rs, and register it in harvest_builder. Where a chapter registers code with a HarvestPlugin method, such as .signals(..) or .dags(..), call the HarvestBuilder method with the same name.

These chapters differ on this path: