Version: 1.1
Status: Specification
This document defines the convention for organizing, exporting, and importing orchestrations and activities across Duroxide library crates. Following this pattern enables modular composition of workflows from multiple domain-specific libraries.
Naming Convention: {crate-name}::{type}::{name}
Example:
duroxide-azure-arm::orchestration::provision-postgresduroxide-azure-arm::activity::provision-vm
Instructions for creating Duroxide library crates (e.g., duroxide-azure-arm, duroxide-aws-ec2)
Organize your crate as follows:
duroxide-azure-arm/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── names.rs # Orchestration name constants (centralized)
│ ├── types.rs # Orchestration input/output types
│ ├── activity_types.rs # Activity input/output types
│ ├── registry.rs # Registry builders
│ ├── inventory.rs # Discovery API (optional)
│ ├── orchestrations/
│ │ ├── mod.rs
│ │ ├── provision_postgres.rs
│ │ └── deploy_webapp.rs
│ └── activities/
│ ├── mod.rs
│ ├── provision_vm.rs # Contains NAME + activity fn
│ └── configure_firewall.rs
└── README.md
Create src/names.rs with const strings for orchestrations only:
//! Name constants for orchestrations
//!
//! Note: Activity names are co-located with their implementations
//! in the activities/ module for better IDE navigation (F12 support).
/// Orchestration names
pub mod orchestrations {
/// Provision an Azure PostgreSQL database
///
/// **Input:** [`crate::types::ProvisionPostgresInput`]
/// **Output:** [`crate::types::ProvisionPostgresOutput`]
/// **Activities used:**
/// - [`crate::activities::provision_vm::NAME`]
/// - [`crate::activities::configure_firewall::NAME`]
pub const PROVISION_POSTGRES: &str = "duroxide-azure-arm::orchestration::provision-postgres";
/// Deploy an Azure Web App
pub const DEPLOY_WEBAPP: &str = "duroxide-azure-arm::orchestration::deploy-webapp";
}Why centralized orchestration names?
- Orchestrations are referenced externally (client.start_orchestration)
- Having them in one file makes discovery easy
- Consumers import
names::orchestrations::PROVISION_POSTGRES
Activity names live in each activity file, not a centralized names file. This enables IDE navigation (F12 jumps to implementation):
// src/activities/provision_vm.rs
use duroxide::ActivityContext;
use crate::activity_types::{ProvisionVMInput, ProvisionVMOutput};
/// Activity name for registration and scheduling
///
/// **Input:** [`ProvisionVMInput`]
/// **Output:** [`ProvisionVMOutput`]
/// **Idempotent:** Yes
pub const NAME: &str = "duroxide-azure-arm::activity::provision-vm";
pub async fn activity(
ctx: ActivityContext,
input: ProvisionVMInput,
) -> Result<ProvisionVMOutput, String> {
ctx.trace_info(format!("Provisioning VM: {}", input.name));
// Implementation...
Ok(ProvisionVMOutput {
vm_id: "vm-123".to_string(),
ip_address: "10.0.0.4".to_string(),
})
}Benefits:
- F12 in IDE → jumps directly to implementation
- Name can't get out of sync with implementation
- Doc comments on NAME are right next to the code
Requirements:
- Use your crate name as the prefix (e.g.,
duroxide-azure-arm) - Use
::activity::for activities - Use kebab-case for names (e.g.,
provision-vm, notProvisionVM)
Create separate files for orchestration and activity types:
src/types.rs - Orchestration types:
//! Input and output types for orchestrations
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvisionPostgresInput {
pub database_name: String,
pub resource_group: String,
pub sku: String,
pub admin_username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvisionPostgresOutput {
pub server_id: String,
pub connection_string: String,
pub admin_password: String,
}src/activity_types.rs - Activity types:
//! Input and output types for activities
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvisionVMInput {
pub name: String,
pub resource_group: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvisionVMOutput {
pub vm_id: String,
pub ip_address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirewallConfig {
pub resource_group: String,
pub rules: Vec<FirewallRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirewallRule {
pub name: String,
pub port: u16,
pub protocol: String,
}Requirements:
- All types must implement
SerializeandDeserialize - Use descriptive struct names
- Document fields with doc comments
- Version types when breaking changes occur (e.g.,
ProvisionPostgresInputV2)
Create orchestration functions in src/orchestrations/ using typed signatures:
// src/orchestrations/provision_postgres.rs
use duroxide::OrchestrationContext;
use crate::activities;
use crate::types::{ProvisionPostgresInput, ProvisionPostgresOutput};
use crate::activity_types::ProvisionVMInput;
pub async fn provision_postgres_orchestration(
ctx: OrchestrationContext,
input: ProvisionPostgresInput,
) -> Result<ProvisionPostgresOutput, String> {
ctx.trace_info(format!("Provisioning PostgreSQL: {}", input.database_name));
// Call activities using NAME from the activity module
let vm_input = ProvisionVMInput {
name: format!("{}-vm", input.database_name),
resource_group: input.resource_group.clone(),
};
let vm_output = ctx
.schedule_activity_typed(activities::provision_vm::NAME, vm_input)
.await?;
// Build output
Ok(ProvisionPostgresOutput {
server_id: vm_output.vm_id,
connection_string: format!("postgresql://{}:5432/{}", vm_output.ip_address, input.database_name),
admin_password: "generated".to_string(),
})
}Requirements:
- Accept typed input, return
Result<TypedOutput, String> - Use activity NAME constants from the activities module
- Add trace logging for observability
Activities have their NAME co-located with implementation:
// src/activities/provision_vm.rs
use duroxide::ActivityContext;
use crate::activity_types::{ProvisionVMInput, ProvisionVMOutput};
/// Activity name for registration and scheduling
pub const NAME: &str = "duroxide-azure-arm::activity::provision-vm";
pub async fn activity(
ctx: ActivityContext,
input: ProvisionVMInput,
) -> Result<ProvisionVMOutput, String> {
ctx.trace_info(format!("Provisioning VM: {}", input.name));
// Check idempotency - does resource already exist?
if let Some(existing) = get_existing_vm(&input.name).await? {
ctx.trace_info("VM already exists, returning existing");
return Ok(existing);
}
// Create the resource
let vm = create_vm(&input).await?;
ctx.trace_info("VM provisioned successfully");
Ok(ProvisionVMOutput {
vm_id: vm.id,
ip_address: vm.ip,
})
}Requirements:
- Define
pub const NAME: &strat the top of each activity file - Accept
ActivityContextas first parameter, then typed input - Return
Result<TypedOutput, String> - Make activities idempotent when possible
- Use
ctx.trace_*()for logging with automatic correlation IDs
Create src/registry.rs using register_typed():
//! Registry builders for exporting orchestrations and activities
use duroxide::OrchestrationRegistry;
use duroxide::runtime::registry::ActivityRegistry;
use crate::names::orchestrations;
use crate::activities;
/// Create an OrchestrationRegistry with all orchestrations from this crate.
///
/// Consumers can merge this into their own registry using `.merge()`.
pub fn create_orchestration_registry() -> OrchestrationRegistry {
OrchestrationRegistry::builder()
.register_typed(
orchestrations::PROVISION_POSTGRES,
crate::orchestrations::provision_postgres::provision_postgres_orchestration,
)
.register_typed(
orchestrations::DEPLOY_WEBAPP,
crate::orchestrations::deploy_webapp::deploy_webapp_orchestration,
)
.build()
}
/// Create an ActivityRegistry with all activities from this crate.
///
/// Consumers can merge this into their own registry using `.merge()`.
pub fn create_activity_registry() -> ActivityRegistry {
ActivityRegistry::builder()
// Use NAME from each activity module
.register_typed(
activities::provision_vm::NAME,
activities::provision_vm::activity,
)
.register_typed(
activities::configure_firewall::NAME,
activities::configure_firewall::activity,
)
.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_orchestration_registry_can_be_created() {
let _registry = create_orchestration_registry();
}
#[test]
fn test_activity_registry_can_be_created() {
let _registry = create_activity_registry();
}
}Key points:
- Use
register_typed()for automatic serde - no manual JSON serialization! - Reference activity names via
activities::module_name::NAME - Reference orchestration names via
orchestrations::CONSTANT
Export all modules in src/lib.rs:
//! Azure ARM Orchestrations and Activities for Duroxide
//!
//! This crate provides orchestrations and activities for provisioning and managing
//! Azure resources using Duroxide's durable orchestration framework.
//!
//! # Quick Start
//!
//! ```rust,no_run
//! use duroxide_azure_arm::registry::{create_orchestration_registry, create_activity_registry};
//! use duroxide_azure_arm::names::orchestrations;
//! use std::sync::Arc;
//!
//! // Load all orchestrations and activities
//! let orchestrations = create_orchestration_registry();
//! let activities = create_activity_registry();
//!
//! // Use with Duroxide runtime
//! // Runtime::start_with_store(store, activities, orchestrations).await;
//! ```
//!
//! # Available Orchestrations
//!
//! - [`names::orchestrations::PROVISION_POSTGRES`] - Provision PostgreSQL database
//! - [`names::orchestrations::DEPLOY_WEBAPP`] - Deploy web application
//!
//! # Available Activities
//!
//! - [`activities::provision_vm::NAME`] - Provision Azure VM
//! - [`activities::configure_firewall::NAME`] - Configure firewall rules
pub mod names;
pub mod types;
pub mod activity_types;
pub mod orchestrations;
pub mod activities;
pub mod registry;
// Optional: runtime discovery
pub mod inventory;
// Re-export commonly used types for convenience
pub use types::*;
pub use activity_types::*;Organize src/activities/mod.rs:
//! Activities for Azure ARM operations
//!
//! Each activity module exports:
//! - `NAME`: The activity name constant for registration
//! - `activity`: The async activity function
pub mod provision_vm;
pub mod configure_firewall;
// Submodules for related activities
pub mod storage;
pub mod networking;Create src/inventory.rs for runtime discovery:
//! Inventory of all orchestrations and activities for discovery
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestrationInfo {
pub name: &'static str,
pub description: &'static str,
pub input_type: &'static str,
pub output_type: &'static str,
pub activities_used: Vec<&'static str>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivityInfo {
pub name: &'static str,
pub description: &'static str,
pub input_type: &'static str,
pub output_type: &'static str,
pub idempotent: bool,
}
pub fn list_orchestrations() -> Vec<OrchestrationInfo> {
vec![
OrchestrationInfo {
name: crate::names::orchestrations::PROVISION_POSTGRES,
description: "Provision an Azure PostgreSQL database",
input_type: "ProvisionPostgresInput",
output_type: "ProvisionPostgresOutput",
activities_used: vec![
crate::activities::provision_vm::NAME,
crate::activities::configure_firewall::NAME,
],
},
]
}
pub fn list_activities() -> Vec<ActivityInfo> {
vec![
ActivityInfo {
name: crate::activities::provision_vm::NAME,
description: "Provision an Azure VM",
input_type: "ProvisionVMInput",
output_type: "ProvisionVMOutput",
idempotent: true,
},
]
}Your crate's README.md should include:
# duroxide-azure-arm
Azure ARM orchestrations and activities for Duroxide.
## Available Orchestrations
- `duroxide-azure-arm::orchestration::provision-postgres` - Provision PostgreSQL database
- `duroxide-azure-arm::orchestration::deploy-webapp` - Deploy web application
## Available Activities
- `duroxide-azure-arm::activity::provision-vm` - Provision Azure VM (idempotent)
- `duroxide-azure-arm::activity::configure-firewall` - Configure firewall rules (idempotent)
## Usage
See examples below for importing into your Duroxide application.[package]
name = "duroxide-azure-arm"
version = "0.1.0"
edition = "2021"
description = "Azure ARM orchestrations and activities for Duroxide"
[dependencies]
duroxide = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
# Add your cloud SDK dependencies here
# azure_mgmt_compute = "0.1"Instructions for using Duroxide library crates in your applications
Simplest approach - load all orchestrations and activities from a single library:
use duroxide::runtime::Runtime;
use duroxide::providers::sqlite::SqliteProvider;
use duroxide_azure_arm::registry::{create_orchestration_registry, create_activity_registry};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Arc::new(SqliteProvider::new("sqlite:./data.db", None).await?);
// Import pre-built registries
let orchestrations = create_orchestration_registry();
let activities = create_activity_registry();
let runtime = Runtime::start_with_store(store, activities, orchestrations).await;
// Your orchestrations and activities are now available
Ok(())
}Combine orchestrations and activities from multiple libraries using .merge():
use duroxide::{OrchestrationRegistry, Runtime};
use duroxide::runtime::registry::ActivityRegistry;
use duroxide::providers::sqlite::SqliteProvider;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Arc::new(SqliteProvider::new("sqlite:./data.db", None).await?);
// Compose orchestrations from multiple crates
let orchestrations = OrchestrationRegistry::builder()
.merge(duroxide_azure_arm::registry::create_orchestration_registry())
.merge(duroxide_aws_ec2::registry::create_orchestration_registry())
.merge(my_workflows::registry::create_orchestration_registry())
.build();
// Compose activities from multiple crates
let activities = ActivityRegistry::builder()
.merge(duroxide_azure_arm::registry::create_activity_registry())
.merge(duroxide_aws_ec2::registry::create_activity_registry())
.merge(my_activities::registry::create_activity_registry())
.build();
let runtime = Runtime::start_with_store(store, activities, orchestrations).await;
Ok(())
}Import only specific orchestrations and activities you need:
use duroxide::{OrchestrationRegistry, Client};
use duroxide::runtime::registry::ActivityRegistry;
use duroxide_azure_arm::names::orchestrations;
use duroxide_azure_arm::{orchestrations as orch_impl, activities};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Arc::new(SqliteProvider::new("sqlite:./data.db", None).await?);
// Import only specific items
let orchestrations = OrchestrationRegistry::builder()
.register_typed(
orchestrations::PROVISION_POSTGRES,
orch_impl::provision_postgres::provision_postgres_orchestration,
)
// Don't import DEPLOY_WEBAPP - we don't need it
.build();
let activities = Arc::new(
ActivityRegistry::builder()
.register_typed(
activities::provision_vm::NAME,
activities::provision_vm::activity,
)
.register_typed(
activities::configure_firewall::NAME,
activities::configure_firewall::activity,
)
.build()
);
let runtime = Runtime::start_with_store(store.clone(), activities, orchestrations).await;
Ok(())
}Start orchestrations using name constants with typed input:
use duroxide::Client;
use duroxide_azure_arm::names::orchestrations;
use duroxide_azure_arm::types::ProvisionPostgresInput;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(store);
// Create strongly-typed input
let input = ProvisionPostgresInput {
database_name: "my-production-db".to_string(),
resource_group: "production-rg".to_string(),
sku: "Standard_D2s_v3".to_string(),
admin_username: "dbadmin".to_string(),
};
// Start orchestration with typed input
client.start_orchestration_typed(
"postgres-prod-1", // instance ID
orchestrations::PROVISION_POSTGRES, // orchestration name
input, // typed input (auto-serialized)
).await?;
// Wait for completion with typed output
let output: ProvisionPostgresOutput = client.wait_for_orchestration_typed(
"postgres-prod-1",
std::time::Duration::from_secs(300),
).await?;
println!("Connection string: {}", output.connection_string);
Ok(())
}Build orchestrations that use orchestrations from other crates:
// In your application crate
use duroxide::OrchestrationContext;
use duroxide_azure_arm::names::orchestrations as azure;
use duroxide_aws_ec2::names::orchestrations as aws;
async fn deploy_multi_cloud_app(
ctx: OrchestrationContext,
input: DeployMultiCloudInput,
) -> Result<DeployMultiCloudOutput, String> {
// Deploy database on Azure
let db_result = ctx
.schedule_sub_orchestration_typed(
azure::PROVISION_POSTGRES,
"db-instance",
db_config,
)
.await?;
// Deploy compute on AWS
let compute_result = ctx
.schedule_sub_orchestration_typed(
aws::CREATE_EC2_CLUSTER,
"app-cluster",
cluster_config,
)
.await?;
Ok(DeployMultiCloudOutput {
db_connection: db_result.connection_string,
cluster_endpoint: compute_result.endpoint,
})
}
// Register your custom orchestration alongside imported ones using .merge()
let orchestrations = OrchestrationRegistry::builder()
.merge(duroxide_azure_arm::registry::create_orchestration_registry())
.merge(duroxide_aws_ec2::registry::create_orchestration_registry())
.register_typed("my-app::orchestration::deploy-multi-cloud", deploy_multi_cloud_app)
.build();Query available orchestrations and activities:
use duroxide_azure_arm::inventory;
// List all available orchestrations
for orch in inventory::list_orchestrations() {
println!("{}", orch.name);
println!(" Description: {}", orch.description);
println!(" Input: {}", orch.input_type);
println!(" Output: {}", orch.output_type);
println!(" Uses activities: {:?}", orch.activities_used);
}
// List all available activities
for activity in inventory::list_activities() {
println!("{}", activity.name);
println!(" Idempotent: {}", activity.idempotent);
}// Good: Descriptive, kebab-case, prefixed with crate
"duroxide-azure-arm::orchestration::provision-postgres"
"duroxide-azure-arm::activity::configure-firewall"
"duroxide-aws-ec2::orchestration::create-vpc"
// Good: Hierarchical organization
"duroxide-stripe::orchestration::process-payment"
"duroxide-stripe::activity::create-charge"
"duroxide-stripe::activity::refund-charge"
// Good: Consistent prefix for ALL items in a crate
// All activities from duroxide-azure-arm use "duroxide-azure-arm::activity::"// Bad: No crate prefix (collision risk)
"provision-postgres"
// Bad: CamelCase or PascalCase
"duroxide-azure-arm::orchestration::ProvisionPostgres"
// Bad: Inconsistent separators
"duroxide-azure-arm/orchestration/provision-postgres"
"duroxide-azure-arm.orchestration.provision-postgres"
// Bad: Generic names
"duroxide-azure-arm::orchestration::workflow1"
// Bad: Inconsistent crate prefixes within same crate
"duroxide-azure-arm::activity::provision-vm"
"azure-activities::activity::configure-firewall" // Different prefix!Test your orchestrations and activities:
#[cfg(test)]
mod tests {
use super::*;
use duroxide::Client;
use duroxide::providers::sqlite::SqliteProvider;
use std::sync::Arc;
#[tokio::test]
async fn test_provision_postgres_orchestration() {
let store = Arc::new(SqliteProvider::new_in_memory().await.unwrap());
let orchestrations = crate::registry::create_orchestration_registry();
let activities = crate::registry::create_activity_registry();
let runtime = Runtime::start_with_store(
store.clone(),
activities,
orchestrations,
).await;
let client = Client::new(store);
let input = ProvisionPostgresInput {
database_name: "test-db".to_string(),
resource_group: "test-rg".to_string(),
sku: "Basic".to_string(),
admin_username: "admin".to_string(),
};
client.start_orchestration_typed(
"test-instance",
crate::names::orchestrations::PROVISION_POSTGRES,
input,
).await.unwrap();
// Wait and verify
let output: ProvisionPostgresOutput = client.wait_for_orchestration_typed(
"test-instance",
std::time::Duration::from_secs(10),
).await.unwrap();
assert!(!output.connection_string.is_empty());
runtime.shutdown(None).await;
}
}Follow semver for your library crate:
- MAJOR: Breaking changes to orchestration/activity signatures
- MINOR: New orchestrations or activities added
- PATCH: Bug fixes, internal improvements
When making breaking changes to input/output types:
// v1
pub struct ProvisionPostgresInput {
pub database_name: String,
}
// v2 (breaking change)
pub struct ProvisionPostgresInputV2 {
pub database_name: String,
pub region: String, // New required field
}
// Register both versions
pub const PROVISION_POSTGRES_V1: &str = "duroxide-azure-arm::orchestration::provision-postgres@v1";
pub const PROVISION_POSTGRES_V2: &str = "duroxide-azure-arm::orchestration::provision-postgres@v2";Or use Duroxide's built-in versioning:
// Register with explicit version
builder.register_versioned_typed(
"duroxide-azure-arm::orchestration::provision-postgres",
"2.0.0",
provision_postgres_v2_orchestration,
)Before publishing your Duroxide library crate:
- All orchestration and activity names follow
{crate}::{type}::{name}pattern - Orchestration names centralized in
names.rs - Activity names co-located with implementations (each file has
pub const NAME) - Input/output types are strongly-typed with serde
- Registry uses
register_typed()for type-safe registration - Documentation includes input/output types for each orchestration/activity
- Activities document whether they're idempotent
- Unit tests verify orchestrations work end-to-end
- README lists all available orchestrations and activities
- Cargo.toml has correct duroxide dependency
- All names use consistent crate prefix
Pass configuration to activities through dependency injection:
// Library provides a factory
pub fn create_activity_registry_with_config(azure_config: AzureConfig) -> ActivityRegistry {
let config = Arc::new(azure_config);
ActivityRegistry::builder()
.register_typed(activities::provision_vm::NAME, {
let config = config.clone();
move |ctx: ActivityContext, input: ProvisionVMInput| {
let config = config.clone();
async move {
ctx.trace_info("Provisioning VM with Azure SDK");
let client = AzureClient::new(&config);
// Use client...
Ok(ProvisionVMOutput { /* ... */ })
}
}
})
.build()
}
// Consumer configures
let config = AzureConfig::from_env();
let activities = duroxide_azure_arm::registry::create_activity_registry_with_config(config);Define domain-specific error types:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AzureError {
ResourceNotFound { resource_id: String },
QuotaExceeded { resource_type: String },
AuthenticationFailed,
}
// Activities can return rich errors (serialized to String)
pub async fn activity(
ctx: ActivityContext,
input: ProvisionVMInput,
) -> Result<ProvisionVMOutput, String> {
// ... on error:
ctx.trace_error("Quota exceeded for VM provisioning");
Err(serde_json::to_string(&AzureError::QuotaExceeded {
resource_type: "VM".to_string(),
}).unwrap())
}Make activities idempotent by checking existing state:
pub async fn activity(
ctx: ActivityContext,
input: ProvisionVMInput,
) -> Result<ProvisionVMOutput, String> {
// Check if VM already exists (idempotency)
if let Some(existing_vm) = azure_client.get_vm(&input.name).await? {
ctx.trace_info("VM already exists, returning existing");
return Ok(ProvisionVMOutput {
vm_id: existing_vm.id,
ip_address: existing_vm.ip,
});
}
// Create only if it doesn't exist
ctx.trace_info(format!("Creating new VM: {}", input.name));
let vm = azure_client.create_vm(input).await?;
Ok(ProvisionVMOutput {
vm_id: vm.id,
ip_address: vm.ip,
})
}A: No, if you follow the naming convention. Each crate prefixes names with its own crate name (e.g., duroxide-azure-arm::, duroxide-aws-ec2::).
A: For IDE navigation. When you F12 on activities::provision_vm::NAME in the registry, you jump directly to the implementation file. Centralized names require an extra hop.
A:
- Activity: Single-purpose operation (provision one VM, send one email)
- Sub-orchestration: Multi-step workflow that benefits from durability (deploy entire application stack)
A: Pass through configuration or environment variables. Never hardcode in orchestrations or activities.
// Good: Configuration through dependency injection
let activities = create_activity_registry_with_config(AzureConfig {
subscription_id: env::var("AZURE_SUBSCRIPTION_ID")?,
tenant_id: env::var("AZURE_TENANT_ID")?,
});A: Yes, they can register their own implementation with the same name (last registration wins):
let orchestrations = OrchestrationRegistry::builder()
.merge(duroxide_azure_arm::registry::create_orchestration_registry())
.register_typed(
duroxide_azure_arm::names::orchestrations::PROVISION_POSTGRES,
my_custom_implementation, // Overrides the library version
)
.build();duroxide-azure-arm- Azure Resource Managerduroxide-aws-ec2- AWS EC2 computeduroxide-aws-s3- AWS S3 storageduroxide-gcp-compute- Google Cloud Computeduroxide-stripe- Stripe payment processingduroxide-sendgrid- SendGrid emailduroxide-twilio- Twilio messagingduroxide-slack- Slack notificationsduroxide-github- GitHub API operationsduroxide-k8s- Kubernetes orchestrations
- Create
names.rswith orchestration constants (centralized for external reference) - Put activity
NAMEconstants in each activity file (for IDE navigation) - Create
types.rsfor orchestration types,activity_types.rsfor activity types - Use
register_typed()in registry for automatic serde - Implement typed orchestrations and activities
- Document all exports with input/output types
- Test thoroughly
Key points:
- Orchestration names → centralized in
names.rs - Activity names → co-located with implementation (
pub const NAME) - Use
register_typed()→ no manual JSON serialization
- Add library to
Cargo.toml - Use
.merge()to compose registries from multiple crates:OrchestrationRegistry::builder() .merge(lib1::create_orchestration_registry()) .merge(lib2::create_orchestration_registry()) .build()
- Use name constants when starting orchestrations
- Use typed methods for automatic serde
Key features:
.merge()- Combine registries from multiple cratesregister_typed()/start_orchestration_typed()- Type-safe with auto-serde- Activity names via
activities::module::NAME- IDE navigation works
This pattern enables a rich ecosystem of reusable Duroxide workflows!