Creating Custom Actions
Extend ARO with custom actions. Actions are the fundamental building blocks that
implement ARO verbs like Extract, Create,
Return, etc.
Understanding Actions
In ARO, every statement follows the Action-Result-Object pattern:
Verb the <result> from/to/with/for the <object>.
Each verb maps to an action implementation that:
- Receives structured information about the statement
- Executes business logic
- Returns a result to be bound to a variable
The ActionImplementation Protocol
public protocol ActionImplementation: Sendable {
/// The semantic role of this action
static var role: ActionRole { get }
/// The verbs that trigger this action
static var verbs: Set<String> { get }
/// Valid prepositions for object clauses
static var validPrepositions: Set<Preposition> { get }
/// Required initializer
init()
/// Execute the action
func execute(
result: ResultDescriptor,
object: ObjectDescriptor,
context: ExecutionContext
) async throws -> any Sendable
}
Key Points
- Sendable: Actions must be thread-safe
- Static properties: Define metadata at compile time
- Async/throws: Actions can be async and may throw errors
- Returns
any Sendable: Results must be sendable across concurrency domains
Action Roles
Actions are categorized by semantic role:
| Role | Description | Example Verbs |
|---|---|---|
request | Request data from external sources | Extract, Retrieve, Request |
own | Create or modify owned data | Create, Compute, Transform, Validate |
response | Send results or responses | Return, Respond, Reply |
export | Export or publish data | Store, Publish, Log, Send |
Step-by-Step: Creating a Custom Action
Step 1: Define Your Action
import ARORuntime
public struct EmailAction: ActionImplementation {
// 1. Define the semantic role
public static let role: ActionRole = .export
// 2. Define verbs that trigger this action
public static let verbs: Set<String> = ["Email", "Mail"]
// 3. Define valid prepositions
public static let validPrepositions: Set<Preposition> = [.to, .with]
// 4. Required initializer
public init() {}
// 5. Implement execute
public func execute(
result: ResultDescriptor,
object: ObjectDescriptor,
context: ExecutionContext
) async throws -> any Sendable {
// Implementation here
}
}
Step 2: Implement the Execute Method
public func execute(
result: ResultDescriptor,
object: ObjectDescriptor,
context: ExecutionContext
) async throws -> any Sendable {
// Get required service
guard let emailService = context.service(EmailService.self) else {
throw ActionError.serviceNotFound("EmailService")
}
// Get the email content
let content: EmailContent = try context.require(result.identifier)
// Get the recipient from the object
let recipient: String
switch object.sourceType {
case .variable:
recipient = try context.require(object.identifier)
case .literal:
recipient = object.identifier
default:
throw ActionError.invalidObjectSource(object.sourceType)
}
// Perform the action
let sendResult = try await emailService.send(
content: content,
to: recipient
)
// Emit event for observability
context.emit(EmailSentEvent(
recipient: recipient,
messageId: sendResult.messageId
))
return sendResult
}
Step 3: Register Your Action
// In your application setup
ActionRegistry.shared.register(EmailAction.self)
Step 4: Use in ARO
(Send Welcome Email: User Onboarding) {
Create the <email-content> with {
subject: "Welcome to our platform!",
body: "Thanks for signing up..."
}.
Extract the <user-email> from the <user: email>.
Email the <email-content> to the <user-email>.
Return an <OK: status> for the <email>.
}
Execution Context
The ExecutionContext provides access to runtime services:
public protocol ExecutionContext: AnyObject, Sendable {
// Variable Management
func resolve<T: Sendable>(_ name: String) -> T?
func require<T: Sendable>(_ name: String) throws -> T
func bind(_ name: String, value: any Sendable)
func exists(_ name: String) -> Bool
// Service Access
func service<S>(_ type: S.Type) -> S?
// Event Emission
func emit(_ event: any RuntimeEvent)
// Metadata
var featureSetName: String { get }
var executionId: String { get }
}
Best Practices
Single Responsibility
Each action should do one thing well:
// Good: Focused action
public struct HashPasswordAction: ActionImplementation { ... }
// Bad: Action doing too much
public struct UserManagementAction: ActionImplementation { ... }
Fail Fast with Descriptive Errors
public func execute(...) async throws -> any Sendable {
// Validate required services
guard let service = context.service(MyService.self) else {
throw ActionError.serviceNotFound("MyService")
}
// Validate required variables
let input: InputType = try context.require(result.identifier)
// ... proceed with execution
}
Emit Events for Observability
// Emit events for significant operations
context.emit(PaymentProcessedEvent(
amount: amount,
currency: currency,
transactionId: result.id
))
Example: External API Action
public struct WeatherAction: ActionImplementation {
public static let role: ActionRole = .request
public static let verbs: Set<String> = ["Weather", "Forecast"]
public static let validPrepositions: Set<Preposition> = [.forPrep]
public init() {}
public func execute(
result: ResultDescriptor,
object: ObjectDescriptor,
context: ExecutionContext
) async throws -> any Sendable {
guard let httpClient = context.service(HTTPClientService.self) else {
throw ActionError.serviceNotFound("HTTPClientService")
}
let city: String = try context.require(object.identifier)
let url = "https://api.weather.com/v1/forecast?city=\(city)"
let response = try await httpClient.get(url: url)
context.bind(result.identifier, value: response)
return response
}
}
Usage:
Weather the <forecast> for the <city>.
Summary
Creating custom actions involves implementing the ActionImplementation protocol,
defining role, verbs, and valid prepositions, implementing the async execute method,
and registering with ActionRegistry.