Skip to content

Application Operations

Overview

The App Engine provides two execution mechanisms:

Command = an application intent — "do X"
Task    = a managed work instance — "X is being performed with lifecycle"

They are not interchangeable. Each serves a different purpose:

Command Task
Represents Intent / capability Work instance
Lifecycle Fire-and-forget Pending → Running → Completed/Failed
Has progress No Yes (via TaskOutputChannel)
Cancellable No (just don't execute) Yes (CancellationToken)
Pausable No Yes (if supported)
Scheduled No (directly) Yes (via Scheduler)
Panic-isolated Yes (catch_unwind) Yes (catch_unwind)
Good for Short, synchronous operations Long, managed, observable operations

Two Execution Paths

Simple Operation

For short, synchronous operations:

flowchart TD
    A["Command"] --> B["CommandExecutor<br/>(panic-isolated)"]
    B --> C["Domain Function<br/>(via CommandHandler)"]
    C --> D["CommandResult"]

Managed Operation

For work that needs lifecycle, progress, cancellation, or scheduling:

flowchart TD
    A["Command"] --> B["Task<br/>(created by CommandHandler or domain code)"]
    B --> C["TaskManager.start()"]
    C --> D["TaskExecutor<br/>(panic-isolated)"]
    D --> E["Domain Function<br/>(via TaskHandler)"]
    E --> F["TaskResult"]

Commands

Defining a Command

A Command is an executable intent — it carries a definition ID, a context, and type-erased input:

use app_shell::app_engine::{
    Command, CommandContext, CommandInput, ExecutionId,
};

let exec_id = ExecutionId::new();
let context = CommandContext::new(exec_id)
    .with_application(app_id)
    .with_session(session_id);

let command = Command::new(
    "document.create",
    context,
    CommandInput::new("MyDocument".to_string()),
);

Command Definition

Before a command can be executed, its definition must be registered. A CommandDefinition describes the command's metadata:

use app_shell::app_engine::CommandDefinition;

let definition = CommandDefinition::new(
    "document.create",           // id
    "Create Document",           // name
    "Creates a new document",    // description
)
.with_metadata("category", "file")
.with_version(Version::new(1, 0, 0))
.with_input_schema(PayloadSchema::of::<String>())
.with_output_schema(PayloadSchema::of::<String>());

Registering a Command Handler

Commands are executed through CommandHandler implementations. Domain code provides the handler:

use app_shell::app_engine::{
    CommandHandler, CommandInput, CommandContext, CommandResult, CommandError,
    EngineRef,
};

struct CreateDocumentHandler;

impl CommandHandler for CreateDocumentHandler {
    fn execute(
        &self,
        input: &CommandInput,
        _context: &CommandContext,
        engine: &EngineRef,
    ) -> Result<CommandResult, CommandError> {
        let name: &String = input.get::<String>()
            .ok_or_else(|| CommandError::InvalidArguments("expected document name".to_string()))?;

        // Call domain function
        let document_id = create_document_in_domain(name);

        // Optional: publish an event through EngineRef
        engine.event_bus.publish(
            &Event::new("document.created")
                .with_payload(document_id.clone())
                .with_source("CreateDocumentCommand")
        );

        Ok(CommandResult::success_with(document_id))
    }
}

// Register with the engine
engine.command_executor_mut().register(
    CommandDefinition::new("document.create", "Create Document", "Creates a new document"),
    Box::new(CreateDocumentHandler),
)?;

Executing a Command

let command = Command::new(
    "document.create",
    CommandContext::new(ExecutionId::new()),
    CommandInput::new("MyDocument".to_string()),
);

let result = engine.execute_command(&command)?;

assert!(result.is_success());
assert_eq!(result.output::<String>(), Some(&"doc_42".to_string()));

Command Result

// Success with output
let result = CommandResult::success_with("doc_42".to_string());

// Success without output
let result = CommandResult::success();

// Failure
let result = CommandResult::failure();

// Failure with details
let result = CommandResult::failure_with("insufficient permissions".to_string());

// With metadata
let result = CommandResult::success()
    .with_metadata("duration_ms", "42")
    .with_metadata("trace_id", "abc123");

// Inspect
result.is_success();        // true
result.is_failure();        // false
result.has_output();        // true
result.output::<String>();  // Some("doc_42")
result.error();             // None

Command Errors

match engine.execute_command(&command) {
    Ok(result) => { /* handle result */ }
    Err(CommandError::UnknownCommand { id }) => {
        eprintln!("No command registered: {}", id);
    }
    Err(CommandError::InvalidArguments(msg)) => {
        eprintln!("Bad input: {}", msg);
    }
    Err(CommandError::ExecutionFailed(msg)) => {
        eprintln!("Handler failed: {}", msg);
    }
    Err(e) => eprintln!("Command error: {}", e),
}

Panic Isolation

If a command handler panics, the panic is caught and converted to an error:

struct PanickingHandler;
impl CommandHandler for PanickingHandler {
    fn execute(&self, _: &CommandInput, _: &CommandContext, _: &EngineRef) -> Result<CommandResult, CommandError> {
        panic!("handler crashed!");
    }
}

// Register and execute
engine.command_executor_mut().register(
    CommandDefinition::new("app.fail", "Fail", "Always fails"),
    Box::new(PanickingHandler),
)?;

let cmd = Command::with_empty_input("app.fail", CommandContext::new(ExecutionId::new()));
let result = engine.execute_command(&cmd);

assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("handler panicked: handler crashed!"));
// Engine continues running — no crash, no Mutex poisoning

Type-Erased Input

CommandInput holds Box<dyn Any + Send>. The handler downcasts to its expected type:

// Pass a struct as input
#[derive(Debug)]
struct ExportInput {
    document_id: String,
    format: String,
    output_path: String,
}

let input = CommandInput::new(ExportInput {
    document_id: "doc_42".to_string(),
    format: "pdf".to_string(),
    output_path: "/tmp/export.pdf".to_string(),
});

// In the handler:
let export_input: &ExportInput = input.get::<ExportInput>()
    .ok_or_else(|| CommandError::InvalidArguments("expected ExportInput".to_string()))?;

The App Engine never inspects what's inside CommandInput. It just stores and forwards it.

Serialized Input

For IPC and persistence, CommandInput can carry serialized bytes:

// Create from serialized bytes (for IPC)
let input = CommandInput::from_bytes(
    "my_domain::ExportInput",
    serialized_bytes,
);

// Create with both typed and serialized forms
let input = CommandInput::with_serialized(
    ExportInput { document_id: "doc_42".to_string(), format: "pdf".to_string(), output_path: "/tmp/x.pdf".to_string() },
    serialized_bytes,
);

// Serialize using a registry
let registry = SerializationRegistry::new();
registry.register(Box::new(ExportInputSerializer));

let mut input = CommandInput::new(ExportInput { /* ... */ });
input.ensure_serialized(&registry)?;  // Serializes if not already
let bytes = input.serialized().unwrap();

// Deserialize from bytes
let mut received = CommandInput::from_bytes("my_domain::ExportInput", bytes);
received.ensure_deserialized(&registry)?;
let data: &ExportInput = received.get::<ExportInput>().unwrap();

Tasks

When to Use a Task Instead of a Command

Situation Use
Short, synchronous operation Command
Needs progress tracking Task
Needs cancellation Task
Needs pause/resume Task
Long-running (seconds+) Task
Needs scheduling (delayed, periodic) Task
Needs state (Pending, Running, Failed) Task
Should appear in history as one operation Task

Defining a Task Type

use app_shell::app_engine::TaskDefinition;

let definition = TaskDefinition::new(
    "project.export",                    // id
    "Export Project",                    // name
    "Exports the current project",       // description
)
.with_pause_support()                    // Can be paused/resumed
.with_cancel_support()                   // Can be cancelled
.with_version(Version::new(2, 0, 0))
.with_input_schema(PayloadSchema::of::<ExportProjectInput>())
.with_output_schema(PayloadSchema::of::<ExportResult>());

Registering a Task Handler

use app_shell::app_engine::{
    TaskHandler, TaskInput, TaskContext, TaskResult, TaskError,
    EngineRef,
};

struct ExportProjectHandler;

impl TaskHandler for ExportProjectHandler {
    fn execute(
        &self,
        input: &TaskInput,
        context: &TaskContext,
        engine: &EngineRef,
    ) -> Result<TaskResult, TaskError> {
        let input: &ExportProjectInput = input.get::<ExportProjectInput>()
            .ok_or_else(|| TaskError::ExecutionFailed {
                id: 0, reason: "expected ExportProjectInput".to_string()
            })?;

        // Check cancellation during long loops
        if context.is_cancelled() {
            return Ok(TaskResult::cancelled());
        }

        // Check deadline for timeout
        if context.is_expired() {
            return Err(TaskError::ExecutionFailed {
                id: 0, reason: "timeout".to_string()
            });
        }

        // Report progress (emits task.progress_changed signal)
        context.output().progress(0.5);

        // Publish completion event through EngineRef
        engine.event_bus.publish(
            &Event::new("project.exporting")
                .with_source("ExportProjectHandler")
        );

        // Call domain function
        let result = domain_engine.export_project(
            &input.project_id,
            &input.format,
            &input.output_path,
        )?;

        // Report completion progress
        context.output().progress(1.0);

        Ok(TaskResult::completed_with(result)
            .with_metadata("format", &input.format))
    }
}

// Register
engine.task_manager_mut().register(
    TaskDefinition::new("project.export", "Export Project", "Exports the project")
        .with_cancel_support(),
    Box::new(ExportProjectHandler),
)?;

Creating a Task Instance

use app_shell::app_engine::{
    TaskContext, TaskInput, ExecutionId,
};

let exec_id = ExecutionId::new();
let task_context = TaskContext::new(exec_id)
    .with_application(app_id)
    .with_session(session_id)
    .with_deadline(Deadline::after(Duration::from_secs(30)));

let task_id = engine.task_manager_mut().create(
    "project.export",
    task_context,
    TaskInput::new(ExportProjectInput {
        project_id: "proj_42".to_string(),
        format: "pdf".to_string(),
        output_path: "/tmp/export.pdf".to_string(),
    }),
)?;

// Task starts in Pending state
assert_eq!(engine.task_manager().get_state(task_id), Some(TaskState::Pending));

The TaskManager automatically: - Assigns a unique TaskId - Creates and injects a CancellationToken - Creates and injects a TaskOutputChannel - Sets the task_id on the context

Starting a Task

// Synchronous execution
let result = engine.start_task(task_id)?;

assert!(result.is_completed());
assert_eq!(result.output::<ExportResult>().unwrap().output_path, "/tmp/export.pdf");
assert_eq!(engine.task_manager().get_state(task_id), Some(TaskState::Completed));

Async Execution

For I/O-heavy handlers, use AsyncTaskHandler with the async-runtime feature:

use app_shell::app_engine::{
    AsyncTaskHandler, AsyncTaskFuture, TaskInput, TaskContext,
    TaskResult, TaskError, EngineRef,
};
use std::pin::Pin;

struct AsyncExportHandler;

impl AsyncTaskHandler for AsyncExportHandler {
    fn execute(
        &self,
        input: &TaskInput,
        _context: &TaskContext,
        _engine: &EngineRef,
    ) -> AsyncTaskFuture {
        // Clone what you need before the async boundary
        // (the future must be 'static)
        let project_id = input.get::<String>().cloned().unwrap_or_default();

        Box::pin(async move {
            // Async I/O here
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            Ok(TaskResult::completed_with(project_id))
        })
    }
}

// Register async handler
engine.register_async_task(
    TaskDefinition::new("async.export", "Async Export", "Export asynchronously"),
    Box::new(AsyncExportHandler),
)?;

// Execute asynchronously
let runtime = AsyncRuntime::new()?;
let result = engine.start_task_async(task_id, &runtime)?;

Important: The async handler's execute() is called synchronously to obtain the Box<dyn Future>. The future is then spawned on tokio. The handler must clone/own whatever it needs from EngineRef before returning the future (the future must be 'static).


Task Lifecycle States

flowchart TD
    P["Pending"] -->|"start()"| R["Running"]
    R --> C["Completed (success)"]
    R --> F["Failed (error)"]
    R --> X["Cancelled"]
    R --> PA["Paused"]
    PA -->|"resume()"| R
use app_shell::app_engine::TaskState;

assert!(TaskState::Pending.can_start());
assert!(TaskState::Running.can_pause());
assert!(TaskState::Paused.can_resume());
assert!(TaskState::Running.can_cancel());
assert!(TaskState::Completed.is_terminal());
assert!(TaskState::Failed.is_terminal());

Task Result

// Completed with output
let result = TaskResult::completed_with("/tmp/export.pdf".to_string());

// Completed without output
let result = TaskResult::completed();

// Cancelled
let result = TaskResult::cancelled();

// Failed with error message
let result = TaskResult::failed("disk full");

// Failed with partial output
let result = TaskResult::failed_with("timeout", partial_log_data);

// With metadata
let result = TaskResult::completed()
    .with_metadata("duration_ms", "4200")
    .with_metadata("file_size", "2.4MB");

// Inspect
result.is_completed();      // true
result.is_cancelled();      // false
result.is_failed();         // false
result.status();            // TaskResultStatus::Completed
result.output::<String>();  // Some("/tmp/export.pdf")
result.error();             // None

Cancellation

Tasks that support cancellation can be cancelled before or during execution:

Cancel Before Start

let definition = TaskDefinition::new("export", "Export", "Export")
    .with_cancel_support(); // ← required

engine.task_manager_mut().register(definition, Box::new(ExportProjectHandler))?;

let task_id = engine.task_manager_mut().create(
    "export",
    TaskContext::new(ExecutionId::new()),
    TaskInput::new("data".to_string()),
)?;

// Cancel while still Pending
engine.task_manager_mut().cancel(task_id)?;
assert_eq!(
    engine.task_manager().get_state(task_id),
    Some(TaskState::Cancelled),
);

Cancel During Execution (CancellationToken)

struct RenderHandler;
impl TaskHandler for RenderHandler {
    fn execute(&self, _: &TaskInput, ctx: &TaskContext, _: &EngineRef) -> Result<TaskResult, TaskError> {
        for i in 0..1000 {
            // Check cancellation — the token is shared with TaskManager::cancel()
            if ctx.is_cancelled() {
                return Ok(TaskResult::cancelled());
            }
            render_frame(i);
        }
        Ok(TaskResult::completed())
    }
}

When cancel() is called on a Running task: 1. The shared CancellationToken is set (atomic flag) 2. The handler checks ctx.is_cancelled() and returns TaskResult::cancelled() 3. The task state transitions to Cancelled

Cancel Non-Cancellable Task

let definition = TaskDefinition::new("rigid", "Rigid", "Not cancellable");
// .with_cancel_support() NOT called

engine.task_manager_mut().register(definition, Box::new(SomeHandler))?;

let task_id = engine.task_manager_mut().create(
    "rigid",
    TaskContext::new(ExecutionId::new()),
    TaskInput::empty(),
)?;

let result = engine.task_manager_mut().cancel(task_id);
assert!(matches!(result, Err(TaskError::NotCancellable { .. })));

Pause and Resume

Tasks that support pausing can be paused while running and resumed later:

let definition = TaskDefinition::new("render", "Render", "Render scene")
    .with_pause_support()
    .with_cancel_support();

engine.task_manager_mut().register(definition, Box::new(RenderHandler))?;

let task_id = engine.task_manager_mut().create(
    "render",
    TaskContext::new(ExecutionId::new()),
    TaskInput::new("scene_data".to_string()),
)?;

// Start the task
engine.start_task(task_id)?;
assert_eq!(engine.task_manager().get_state(task_id), Some(TaskState::Running));

// Pause (only in Running state)
engine.task_manager_mut().pause(task_id)?;
assert_eq!(engine.task_manager().get_state(task_id), Some(TaskState::Paused));

// Resume
engine.task_manager_mut().resume(task_id)?;
assert_eq!(engine.task_manager().get_state(task_id), Some(TaskState::Running));

When a Command Should Create a Task

The most powerful pattern is a command that creates a task — combining intent with managed execution:

struct StartExportCommand;

impl CommandHandler for StartExportCommand {
    fn execute(&self, input: &CommandInput, _context: &CommandContext, engine: &EngineRef) -> Result<CommandResult, CommandError> {
        let export_input: &ExportProjectInput = input.get::<ExportProjectInput>()
            .ok_or_else(|| CommandError::InvalidArguments("expected ExportProjectInput".to_string()))?;

        // Instead of exporting directly, delegate to the task system
        // (In a real app, the engine would be passed differently)
        Ok(CommandResult::success()
            .with_metadata("task_definition", "project.export"))
    }
}

Decision Flowchart

flowchart TD
    Q1{"Is the operation short and synchronous?"} -->|"Yes"| A["Command (direct execution)"]
    Q1 -->|"No"| Q2{"Does it need progress tracking?"}
    Q2 -->|"Yes"| B["Task"]
    Q2 -->|"No"| Q3{"Does it need cancellation?"}
    Q3 -->|"Yes"| C["Task"]
    Q3 -->|"No"| Q4{"Does it need scheduling (delayed/periodic)?"}
    Q4 -->|"Yes"| D["Task + Scheduler"]
    Q4 -->|"No"| Q5{"Does it need pause/resume?"}
    Q5 -->|"Yes"| E["Task"]
    Q5 -->|"No"| F["Command (sufficient)"]

Examples

Operation Mechanism Why
Set zoom level Command Instant, no lifecycle needed
Change color Command Instant
Export project Task Long-running, cancellable, has progress
Render scene Task Long-running, pausable
Autosave Task + Scheduler Periodic, scheduled
Load font Resource (via loader) Not a command or task
Undo last operation Command → History Triggers history undo, then re-executes

Managing Multiple Tasks

// Create multiple tasks
let task_a = engine.task_manager_mut().create(
    "project.export",
    TaskContext::new(ExecutionId::new()),
    TaskInput::new(export_a),
)?;

let task_b = engine.task_manager_mut().create(
    "render.scene",
    TaskContext::new(ExecutionId::new()),
    TaskInput::new(render_data),
)?;

assert_eq!(engine.task_manager().count(), 2);

// Start them
engine.start_task(task_a)?;
engine.start_task(task_b)?;

// Verify states
assert_eq!(engine.task_manager().get_state(task_a), Some(TaskState::Completed));
assert_eq!(engine.task_manager().get_state(task_b), Some(TaskState::Completed));

// Remove completed tasks
engine.task_manager_mut().remove(task_a)?;
assert_eq!(engine.task_manager().count(), 1);

Error Handling

Command Errors

// Unknown command
let cmd = Command::with_empty_input("unknown.cmd", context);
match engine.execute_command(&cmd) {
    Err(CommandError::UnknownCommand { id }) => {
        eprintln!("No handler for: {}", id);
    }
    _ => {}
}

// Invalid input (handler rejects)
let cmd = Command::new("document.create", context, CommandInput::new(42_i64)); // wrong type
match engine.execute_command(&cmd) {
    Err(CommandError::InvalidArguments(msg)) => {
        eprintln!("Handler rejected input: {}", msg);
    }
    _ => {}
}

Task Errors

// Task not found
match engine.task_manager_mut().start(TaskId::new()) { // bogus id
    Err(TaskError::NotFound { id }) => {
        eprintln!("Task {} not found", id);
    }
    _ => {}
}

// Invalid state transition
engine.start_task(task_id)?; // → Completed
match engine.task_manager_mut().start(task_id) { // can't start completed task
    Err(TaskError::InvalidState { current, requested, .. }) => {
        eprintln!("Can't start: {} → {}", current, requested);
    }
    _ => {}
}

// Handler failure
struct FailingHandler;
impl TaskHandler for FailingHandler {
    fn execute(&self, _: &TaskInput, _: &TaskContext, _: &EngineRef) -> Result<TaskResult, TaskError> {
        Err(TaskError::ExecutionFailed { id: 0, reason: "disk full".to_string() })
    }
}

// When start() fails, the task transitions to Failed
let result = engine.task_manager_mut().start(failing_task_id);
assert!(result.is_err());
assert_eq!(
    engine.task_manager().get_state(failing_task_id),
    Some(TaskState::Failed),
);

Panic-Isolated Task Errors

struct PanickingTaskHandler;
impl TaskHandler for PanickingTaskHandler {
    fn execute(&self, _: &TaskInput, _: &TaskContext, _: &EngineRef) -> Result<TaskResult, TaskError> {
        panic!("task handler crashed!");
    }
}

// Register and execute
engine.task_manager_mut().register(
    TaskDefinition::new("panic.task", "Panic", "Panics"),
    Box::new(PanickingTaskHandler),
)?;

let task_id = engine.task_manager_mut().create(
    "panic.task",
    TaskContext::new(ExecutionId::new()),
    TaskInput::empty(),
)?;

let result = engine.start_task(task_id);

assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("handler panicked: task handler crashed!"));

// Task is in Failed state
assert_eq!(engine.task_manager().get_state(task_id), Some(TaskState::Failed));

// TaskManager still usable (Mutex not poisoned)
assert_eq!(engine.task_manager().count(), 1);

Complete Example: Command → Task → Result

use app_shell::app_engine::*;

// --- Domain data types ---

#[derive(Debug)]
struct ExportInput {
    project_path: String,
    format: String,
    output_path: String,
}

// --- Task handler (executes the actual work) ---

struct ExportProjectHandler;

impl TaskHandler for ExportProjectHandler {
    fn execute(
        &self,
        input: &TaskInput,
        ctx: &TaskContext,
        engine: &EngineRef,
    ) -> Result<TaskResult, TaskError> {
        let input: &ExportInput = input.get::<ExportInput>()
            .ok_or_else(|| TaskError::ExecutionFailed {
                id: 0, reason: "expected ExportInput".into()
            })?;

        // Check cancellation
        if ctx.is_cancelled() {
            return Ok(TaskResult::cancelled());
        }

        // Report progress
        ctx.output().progress(0.0);

        // Domain function call
        let result = domain_export_project(&input.project_path, &input.format, &input.output_path);

        ctx.output().progress(1.0);

        match result {
            Ok(path) => {
                // Publish completion event
                engine.event_bus.publish(
                    &Event::new("task.completed").with_source("ExportProjectHandler")
                );
                Ok(TaskResult::completed_with(path))
            }
            Err(e) => Err(TaskError::ExecutionFailed {
                id: 0, reason: e.to_string()
            }),
        }
    }
}

// --- Setup ---

fn setup_engine() -> AppEngine {
    let mut engine = Bootstrap::create().unwrap();
    engine.task_manager_mut().register(
        TaskDefinition::new("project.export", "Export Project", "Export the project")
            .with_cancel_support(),
        Box::new(ExportProjectHandler),
    ).unwrap();
    engine
}

// --- Usage ---

fn main() {
    let engine = setup_engine();
    engine.initialize().unwrap();
    engine.start().unwrap();

    // Create a task
    let task_id = engine.task_manager().create(
        "project.export",
        TaskContext::new(ExecutionId::new())
            .with_deadline(Deadline::after(std::time::Duration::from_secs(30))),
        TaskInput::new(ExportInput {
            project_path: "/projects/demo.proj".to_string(),
            format: "pdf".to_string(),
            output_path: "/tmp/demo.pdf".to_string(),
        }),
    ).unwrap();

    // Execute
    let result = engine.start_task(task_id).unwrap();

    assert!(result.is_completed());
    assert_eq!(result.output::<String>(), Some(&"/tmp/demo.pdf".to_string()));

    engine.stop().unwrap();
    engine.dispose().unwrap();
}

Quick Reference

Command API

// Register
engine.command_executor_mut().register(definition, handler)?;

// Execute (convenience method — constructs EngineRef internally)
let result: CommandResult = engine.execute_command(&cmd)?;

// Query
engine.command_executor().contains(&definition_id);   // bool
engine.command_executor().get(&definition_id);        // Option<&CommandDefinition>
engine.command_executor().list();                     // Vec<&CommandDefinition>

Task API

// Register
engine.task_manager_mut().register(definition, handler)?;

// Create (returns TaskId)
let task_id = engine.task_manager_mut().create(definition_id, context, input)?;

// Lifecycle
engine.start_task(task_id)?;     // Pending → Running → Completed/Failed
engine.task_manager_mut().cancel(task_id)?;    // → Cancelled
engine.task_manager_mut().pause(task_id)?;     // Running → Paused
engine.task_manager_mut().resume(task_id)?;    // Paused → Running

// Query
engine.task_manager().get_state(task_id);       // Option<TaskState>
engine.task_manager().count();                  // usize

// Remove
engine.task_manager_mut().remove(task_id);    // Option<Task>

Async API (with async-runtime feature)

// Register async handler
engine.register_async_task(definition, Box::new(AsyncHandler))?;

// Execute asynchronously
let runtime = AsyncRuntime::new()?;
let result = engine.start_task_async(task_id, &runtime)?;

Next Steps