Creating a request flow using the C3Request API
What is the C3Request System?
The C3Request system provides a robust framework for handling approval-based workflows in your C3 AI applications. Think of it as a system that manages:
- Submitting a request to perform an action or gain access to a resource
- Requiring approval or denial from an authorized user
- Triggering a follow-up action based on the decision
Some example use cases include:
- Grant access to restricted data
- Configure user-level settings
- Initiate a resource-intensive process
Basic Data Model
There are two major components in the C3Request data model: C3Request and C3Request.Processor.
The C3Request Entity
The C3Request entity represents the overall request. It includes comprehensive metadata about the request lifecycle:
Core Fields:
- C3Request#topic: Specifies the request type and determines which processor will run followup actions
- C3Request#data: Contains the actual request data or description
- C3Request#state: Current state in the workflow (see C3Request.State)
- C3Request#requestedBy: Identifies the user who submitted the request
- C3Request#decidedBy: Identifies the user who approved or denied the request
- C3Request#received: Records the timestamp when the request was received/created in the system
- C3Request#decided: Records the timestamp when the request was decided upon (approved, denied, canceled)
Processing Status Fields:
- C3Request#processed: Boolean indicating whether the processor lambda ran from start to finish
- C3Request#status: Captures the final status (success or error); see C3Request.Status
- C3Request#result: Stores the result message from the processor lambda
- C3Request#error: Contains the error message the processor or system returned when it encountered a failure
- C3Request#processStart and C3Request#processEnd: Record the start and end times of the processor lambda execution
The C3Request.Processor Entity
The C3Request.Processor entity is responsible for handling the processing logic of a request. Once a request has been approved, denied, or cancelled, the C3Request.Processor is invoked to perform actions based on the decision.
Key Components:
- C3Request.Processor#topic: Must match the request's topic (unique constraint ensures one processor per topic)
- C3Request.Processor#onCancel: Lambda function executed when a request is cancelled
- C3Request.Processor#onApprove: Lambda function executed when a request is approved
- C3Request.Processor#onDeny: Lambda function executed when a request is denied
The lambda functions receive the request entity and return a C3Request.Processor.Result containing:
- C3Request.Processor.Result#status: Either SUCCESS or ERROR
- C3Request.Processor.Result#result: Success message or data. The field is a
stringso any complex structured data should be JSON serialized to astring. - C3Request.Processor.Result#error: Error message or data if processing failed. The field is a
stringso any complex structured data should be JSON serialized to astring.
Understanding Request States
The C3Request.State enum defines the request lifecycle:
- C3Request.State#PENDING: Request is waiting for approval/denial
- C3Request.State#CANCELLING: Request cancelled, processing the cancellation
- C3Request.State#APPROVING: Request approved, processing the approval
- C3Request.State#DENYING: Request denied, processing the denial
- C3Request.State#CANCELLED: Request cancelled and processing completed
- C3Request.State#APPROVED: Request approved and processing completed
- C3Request.State#DENIED: Request denied and processing completed
Creating a Request Flow
Use the C3Request framework to define how your system handles user-driven requests from submission to resolution. Start by registering a processor that encapsulates decision logic, then choose the request structure that fits your use case.
Step 1: Define Your Request Processor
Start by defining a C3Request.Processor record. This can be done through seeded data, through the static console, or via an API call.
Example processor for a simple approval workflow:
In the onDeny and onCancel lambdas, return a SUCCESS status to indicate that the denial lambda executed as expected. The C3Request#status field exists for system logic and the admin — not for the requester.
C3Request.Processor.make({
topic: "increase_single_node_env_allocation",
onApprove: Lambda.fromJsSrc(function(request) {
// Process the approved allocation
var userId = request.requestedBy;
var requestedAllocation = JSON.parse(request.data).allocation;
StudioUserConfig.forId(userId).setConfigValue('maxSingleNodeEnvPerStudioUser', requestedAllocation);
return {
status: "SUCCESS",
result: "Allocation of " + requestedAllocation + " SNEs was granted to " + userId,
};
}),
onDeny: Lambda.fromJsSrc(function(request) {
// Handle the denied allocation
// e.g., notify requester, log denial reason, etc.
// NOTE: Success here signifies that the denial was processed successfully,
// not that the request was approved.
return {
status: "SUCCESS",
result: "Allocation request denied"
};
}),
onCancel: Lambda.fromJsSrc(function(request) {
// Handle the cancelled allocation
// NOTE: Success here signifies that the cancellation was processed successfully,
// not that the request was approved.
return {
status: "SUCCESS",
result: "Allocation request cancelled"
};
})
}).create();If no special logic is required in your C3Request.Processor handling of onDeny and onCancel, there is no needto specify a onDeny or onCancel lambda, the C3Request.Processor can rely on the system default handling of onDeny and onCancel, which simply returns a SUCCESS status.
Step 2: Choose Your Request Type
You have two options for structuring your request data:
Option 1: Use C3GenericRequest
For simple requests where you just need to pass unstructured or simple data, use C3GenericRequest. Store your request data in the C3RequestFields#data field:
C3GenericRequest.create({
topic: "increase_single_node_env_allocation",
data: JSON.stringify({
allocation: 3,
})
});Option 2: Create a Custom Request Type
For more complex requests or less adhoc flows, create a custom entity that mixes C3Request:
entity type StudioSneAllocationRequest mixes C3Request {
allocation: int
}Step 3: Create and Submit Requests
Create requests using the standard C3Request#create method:
var request = StudioSneAllocationRequest.make({
topic: "increase_single_node_env_allocation",
allocation: 3
}).create();The C3Request#beforeCreate method automatically sets appropriate values for every field on C3Request except topic and data. The system will override any values that are present upon creation for those fields.
Approving and Denying Requests
Once you have defined your request and processor, requests can be approved or denied using the C3Request#approve and C3Request#deny methods.
Important: These methods are final. Any custom behavior should be handled by the C3Request.Processor lambda functions.
Approval Process
When C3Request#approve is called:
- Sets C3RequestFields#decided timestamp
- Sets C3RequestFields#decidedBy to current user
- Sets C3RequestFields#deciderComment if provided in C3Request.DeciderSpec.
- Changes C3RequestFields#state to C3Request.State#APPROVING
- Submits request to ActionQueue for processing
Denial Process
When C3Request#deny is called:
- Sets C3RequestFields#decided timestamp
- Sets C3RequestFields#decidedBy to current user
- Sets C3RequestFields#deciderComment if provided in C3Request.DeciderSpec.
- Changes C3RequestFields#state to C3Request.State#DENYING
- Submits request to ActionQueue for processing
Processing Guarantees and Best Practices
Understand how the C3Request system guarantees execution and handles consistency across distributed environments. Build processors with fault tolerance, concurrency safety, and idempotency as core design goals.
Guaranteed Execution
The C3Request system guarantees that the ActionQueue will execute the C3Request.Processor lambda at least once for each request decision. If a node gets interrupted before completing the lambda, the system re-submits the request to the queue and re-executes the lambda from the beginning.
Eventual Consistency Model
The C3Request system follows an eventual consistency model. Because the C3 Agentic AI Platform runs on Kubernetes, any C3 node may restart at any time as the platform rebalances resources across the cluster. With enough load or time increases, the system will exhibit the following behaviors:
- C3Request.Processor lambdas can and will fail at all points of the lambda
- Rarely, the same request might be processed multiple times simultaneously due to network issues
The C3Request system handles these scenarios gracefully and guarantees that the ActionQueue will execute each processor lambda from start to finish at least once. However, you must ensure that every processor lambda remains idempotent and self-healing.
Some operations carry low risk when retried — for example, setting a configuration value produces the same result each time. But if the processor triggers side effects like sending emails, launching external workflows, or modifying database records, you must ensure the lambda handles repeated execution safely. Design it to support retries from any point without introducing inconsistent or duplicate outcomes.
Writing Idempotent Processors
Idempotent means the lambda can be safely run multiple times with the same result. Here is an example on how to achieve this.
If a processor lambda for the same request has 2 ActionQueue jobs running concurrently, and they both finish at the same time, the first to finish will win. If they are significantly offset, the last one to finish will win.
onApprove: function(request) {
// ✅ Good: Check if work was already done
var userId = request.requestedBy;
var currentAllocation = StudioUserConfig.forId(userId).maxSingleNodeEnvPerStudioUser;
var requestedAllocation = JSON.parse(request.data).allocation;
if (currentAllocation == requestedAllocation) {
return {
status: "SUCCESS",
result: "Allocation of " + requestedAllocation + " SNEs was granted to " + userId,
};
}
// ✅ Good: Use upsert/merge/setConfigValue that result in the same end state for fields request cares about
StudioUserConfig.forId(userId).setConfigValue('maxSingleNodeEnvPerStudioUser', requestedAllocation);
return {
status: "SUCCESS",
result: "Allocation of " + requestedAllocation + " SNEs was granted to " + userId,
};
}Error Handling
Always handle errors gracefully and return appropriate C3Request.Processor.Result objects:
onApprove: function(request) {
try {
// Your processing logic here
return {
status: "SUCCESS",
result: "Processing completed successfully"
};
} catch (error) {
return {
status: "ERROR",
error: "Processing failed: " + error.message
};
}
}The request system includes default error handling that catches any error escaping the lambda, but you should handle errors explicitly within the lambda.