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.