Services
Adama documents live in a controlled, deterministic environment. That's by design — determinism is how I get durability and consistency guarantees. But real applications need to talk to the outside world: validate OAuth tokens, send SMS messages, process payments, call HTTP APIs. Services are the bridge.
I designed services specifically for the document execution model. Service calls return futures that can be awaited within procedures, and results are wrapped in result<T> types so you're forced to handle errors. No pretending the network is reliable.
Linking Built-in Services
Adama ships with several built-in services. Use the @link directive to pull one into your document:
@link googlevalidator { }
That single line makes the googlevalidator service available with all its predefined methods and types. Minimal ceremony.
Linking with Configuration
Some services need configuration — secrets, API keys, that sort of thing:
@link googlevalidator {
// Configuration options go here
}
For services requiring credentials, configuration is managed through the platform's secrets system rather than hardcoded in the document. (Never hardcode secrets. I shouldn't have to say this, but here we are.)
Defining Custom Services
For external APIs not covered by built-in services, you can define your own:
message SendRequest {
string phone;
string message;
}
message SendResponse {
bool success;
string messageId;
}
service sms {
internal = "twilio.com";
method<SendRequest, SendResponse> send;
}
Service Definition Anatomy
A service definition has two parts:
- internal: A string identifying the service implementation
- method declarations: Define the request and response types for each callable method
message RequestType {}
message ResponseType {}
message AnotherRequest {}
message AnotherResponse {}
service myservice {
internal = "service-identifier";
method<RequestType, ResponseType> methodName;
method<AnotherRequest, AnotherResponse> anotherMethod;
}
Dynamic Request/Response Types
For services with variable response structures, use dynamic:
service flexibleApi {
internal = "httpjson";
method<dynamic, dynamic> call;
}
// Usage
@web get /example {
let response = flexibleApi.call(@who, {
url: "https://api.example.com/data",
method: "GET"
}.to_dynamic()).await();
return {json: {}};
}
Calling Services
Service calls follow a consistent pattern:
result<ResponseType> = service.method(principal, request).await();
The Call Pattern
- service.method: The service name and method to invoke
- principal: Who is making the request (typically
@whoor@no_one) - request: A message matching the method's request type
- await(): Blocks until the service responds
Example: Google SSO Validation
Here's a complete example validating a Google OAuth token:
@link googlevalidator { }
message GoogleSignin {
string token;
}
@web put /auth/google (GoogleSignin gs) {
// Call the validation service
if (googlevalidator.validate(@who, {token: gs.token}).await() as validated) {
// Success - validated contains the user's email
return {
json: { email: validated.email, name: validated.name },
cors: true
};
}
// Failed to validate
return {
json: { error: "Invalid Google token" },
cors: true
};
}
The googlevalidator.validate method returns the user's email address if the token is valid.
Understanding result
Service calls return result<T> rather than raw values. This is intentional — the network is unreliable, services go down, tokens expire. I wanted the type system to force you to handle failure.
Pattern Matching with as
The most common way to handle results is with the as pattern in an if statement:
message Req {}
message Resp {}
service myservice {
internal = "example";
method<Req, Resp> call;
}
procedure process(Resp r) {}
procedure handleError() {}
@web put /example (Req request) {
if (myservice.call(@who, request).await() as response) {
// Success: response is unwrapped and usable
process(response);
} else {
// Failure: the call did not succeed
handleError();
}
return {json: {}};
}
Result Methods
The result<T> type provides several methods for inspection:
| Method | Return Type | Description |
|---|---|---|
has() |
bool | Returns true if the result contains a value |
finished() |
bool | Returns true if the operation has completed |
failed() |
bool | Returns true if the operation failed |
code() |
int | Returns the error code (0 on success) |
message() |
string | Returns the error message (empty on success) |
as_maybe() |
maybe |
Converts the result to a maybe type |
Explicit Error Handling
For more detailed error handling, skip pattern matching and inspect the result directly:
result<Response> r = service.call(@who, request).await();
if (r.has()) {
// Success - access the value
Response response = r.as_maybe().getOrDefaultTo(defaultResponse);
processResponse(response);
} else {
// Failure - examine the error
int errorCode = r.code();
string errorMessage = r.message();
if (errorCode == 401) {
handleUnauthorized();
} else {
logError(errorCode, errorMessage);
}
}
Using Results in Formulas
Service results can be used in reactive formulas:
message SendRequest {
string phone;
string message;
}
message SendResponse {
bool success;
string messageId;
}
service sms {
internal = "twilio.com";
method<SendRequest, SendResponse> send;
}
public string messageToSend = "Hello";
public formula sendResult = sms.send(@no_one, {
phone: "555-1234",
message: messageToSend
});
// Access result properties reactively
public formula sendSucceeded = sendResult.has();
public formula sendError = sendResult.message();
When messageToSend changes, the formula recomputes and triggers a new service call. This is one of those features that feels almost too magical, but it works.
Built-in Services Reference
Here's what ships with the platform.
googlevalidator
Validates Google OAuth tokens and returns user information.
@link googlevalidator { }
Method: validate
- Request:
{ token: string } - Response:
{ email: string, name: maybe<string>, picture: maybe<string> }
@link googlevalidator { }
message ValidateReq {
string token;
}
@web put /validate (ValidateReq vr) {
string clientToken = vr.token;
if (googlevalidator.validate(@who, {token: clientToken}).await() as user) {
// user.email is guaranteed
// user.name and user.picture are optional
}
return {json: {}};
}
identitysigner
Signs identity tokens for authenticated sessions.
@link identitysigner {
authority = "myauthority";
private_key = "encrypted:key123";
}
twilio
Send SMS messages via Twilio.
@link twilio { }
Method: send
- Request:
{ from: string, to: string, body: string } - Response:
{ sid: string }
sendgrid
Send emails via SendGrid.
@link sendgrid {
api_key = "encrypted:key123";
}
amazonses
Send emails via Amazon Simple Email Service.
@link amazonses {
access_id = "encrypted:id123";
secret_key = "encrypted:key123";
region = "us-east-1";
}
stripe
Process payments via Stripe.
@link stripe {
apikey = "encrypted:sk_test_123";
}
discord
Send messages to Discord channels.
@link discord {
bot_token = "encrypted:token123";
}
jitsi
Create video conference rooms.
@link jitsi {
private_key = "encrypted:key123";
sub = "my-jitsi-app";
}
httpjson
Make arbitrary HTTP requests returning JSON. This is the escape hatch for anything not covered by the other services.
@link httpjson { }
adama
Interact with other Adama documents. Document-to-document communication.
@link adama { }
push
Send push notifications to users.
@link push { }
Method: notify
- Request: auto-generated
_NotifyReqtype with fields likedomain,payload - Response:
{ push_id: string }
@link push { }
record Person {
public int id;
public principal account;
}
table<Person> _people;
int count;
string link;
message NotifyInput {}
@web put /notify (NotifyInput ni) {
maybe<Person> mperson = (iterate _people limit 1)[0];
if (mperson as person) {
_NotifyReq req;
req.domain = @context.domain;
req.payload = {title: "New Message", body: "You have a notification", badge: count, url: link}.to_dynamic();
if (push.notify(person.account, req).await() as result) {
// result.push_id is the tracking ID
}
}
return {json: {}};
}
saferandom
Generate cryptographically secure random values. Because Math.random() isn't good enough when security matters.
@link saferandom { }
Method: ask
- Request:
{ pool: string, count: int } - Response:
{ result: string }
Method: uuidWithHash
- Request: auto-generated empty type
- Response:
{ result: string, hash: string }
@link saferandom { }
message Msg {}
@web put /example (Msg m) {
// Generate a random string from a character pool
if (saferandom.ask(@who, {pool:"0123456789X", count:6}).await() as pw) {
string temp_password = pw.result;
}
// Generate a UUID with its hash
_SafeRandom_Empty empty;
if (saferandom.uuidWithHash(@who, empty).await() as otp) {
string token = otp.result; // the UUID
string hash = otp.hash; // pre-hashed version
}
return {json: {}};
}
delay
Add delays in execution (useful for rate limiting).
@link delay { }
Auto-Generated Service Types
When you link a service, Adama auto-generates request and response types prefixed with _ServiceName_. These types are used for constructing requests:
@link push { }
@link saferandom { }
message Msg {}
@web put /example (Msg m) {
// Auto-generated types become available:
_SafeRandom_Empty empty; // empty request for saferandom
_NotifyReq req; // request type for push.notify()
req.domain = @context.domain;
req.payload = {title: "Hello"}.to_dynamic();
return {json: {}};
}
The auto-generated type names follow the pattern _ServiceName_MethodName or common abbreviations. Check the service documentation for specific type names.
Service Configuration
Services often need API keys and secrets. This is managed through the platform's secrets system rather than hardcoded in documents.
Secrets Management
Secrets are stored securely in the platform and injected at runtime. Configuration within @link blocks references these secrets:
@link twilio {
// Secrets are referenced by name
// The platform injects actual values at runtime
}
Endpoint Configuration
Some services allow endpoint customization for different environments:
service customApi {
internal = "httpjson";
method<dynamic, dynamic> call;
}
The internal identifier tells Adama which service implementation to use, while the platform configuration determines actual endpoints and credentials.
Complete Example: User Authentication
Here's a complete Google SSO integration with user management. This is the kind of thing that takes hundreds of lines in a traditional backend — here it's one file:
@link googlevalidator { }
@link identitysigner {
authority = "myauthority";
private_key = "encrypted:key123";
}
record User {
private principal identity;
public string email;
public string name;
public string picture;
}
table<User> users;
message GoogleToken {
string token;
}
// Handle Google sign-in
@web put /auth/google (GoogleToken gt) {
if (googlevalidator.validate(@who, {token: gt.token}).await() as validated) {
// Check if user exists
list<User> existing = iterate users where email == validated.email limit 1;
if (existing.size() > 0) {
// Existing user - update and return identity
if (validated.name as name) {
(iterate users where email == validated.email limit 1).name = name;
}
if (validated.picture as pic) {
(iterate users where email == validated.email limit 1).picture = pic;
}
return {
sign: validated.email,
cors: true
};
} else {
// New user - create account
users <- {email: validated.email, identity: @who};
if (validated.name as name) {
(iterate users where email == validated.email limit 1).name = name;
}
if (validated.picture as pic) {
(iterate users where email == validated.email limit 1).picture = pic;
}
return {
sign: validated.email,
cors: true
};
}
}
return {
json: { error: "Failed to validate Google token" },
cors: true
};
}
// Get current user profile
@web get /me {
list<User> user = iterate users where identity == @who limit 1;
if (user.size() > 0) {
return {
json: {
email: user[0].email,
name: user[0].name,
picture: user[0].picture
},
cors: true
};
}
return {
json: { error: "Not authenticated" },
cors: true
};
}
Error Handling Patterns
Retry Logic
For transient failures, implement retry logic:
message SendRequest {
string phone;
string message;
}
message SendResponse {
bool success;
string messageId;
}
service sms {
internal = "twilio.com";
method<SendRequest, SendResponse> send;
}
procedure sendWithRetry(string phone, string message) -> bool {
int attempts = 0;
while (attempts < 3) {
if (sms.send(@no_one, {phone: phone, message: message}).await() as result) {
return true;
}
attempts++;
}
return false;
}
Fallback Services
Use multiple services with fallback. The good news is you have options; the bad news is you need them:
@link sendgrid { }
@link amazonses { }
procedure sendEmail(string to, string subject, string body) -> bool {
// Try primary service
if (sendgrid.send(@no_one, {to: to, subject: subject, body: body}).await() as r) {
return true;
}
// Fallback to secondary
if (amazonses.send(@no_one, {to: to, subject: subject, body: body}).await() as r) {
return true;
}
return false;
}
Logging Failures
Track service failures for monitoring:
record ServiceFailure {
public string service;
public int errorCode;
public string errorMessage;
public long timestamp;
}
table<ServiceFailure> failures;
procedure callWithLogging(/* ... */) {
let r = service.call(@who, request).await();
if (!r.has()) {
failures <- {
service: "serviceName",
errorCode: r.code(),
errorMessage: r.message(),
timestamp: Time.datetime()
};
}
}
Services are how documents reach out to the real world while staying within the document execution model. The result<T> wrapper is the honest acknowledgment that external calls can fail — and the as pattern makes handling that failure feel natural rather than painful.