C3 AI Documentation Home

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:

Processing Status Fields:

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:

The lambda functions receive the request entity and return a C3Request.Processor.Result containing:

Understanding Request States

The C3Request.State enum defines the request lifecycle:

  1. C3Request.State#PENDING: Request is waiting for approval/denial
  2. C3Request.State#CANCELLING: Request cancelled, processing the cancellation
  3. C3Request.State#APPROVING: Request approved, processing the approval
  4. C3Request.State#DENYING: Request denied, processing the denial
  5. C3Request.State#CANCELLED: Request cancelled and processing completed
  6. C3Request.State#APPROVED: Request approved and processing completed
  7. 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:

JavaScript
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:

JavaScript
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:

Type
entity type StudioSneAllocationRequest mixes C3Request {
  allocation: int
}

Step 3: Create and Submit Requests

Create requests using the standard C3Request#create method:

JavaScript
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:

  1. Sets C3RequestFields#decided timestamp
  2. Sets C3RequestFields#decidedBy to current user
  3. Sets C3RequestFields#deciderComment if provided in C3Request.DeciderSpec.
  4. Changes C3RequestFields#state to C3Request.State#APPROVING
  5. Submits request to ActionQueue for processing

Denial Process

When C3Request#deny is called:

  1. Sets C3RequestFields#decided timestamp
  2. Sets C3RequestFields#decidedBy to current user
  3. Sets C3RequestFields#deciderComment if provided in C3Request.DeciderSpec.
  4. Changes C3RequestFields#state to C3Request.State#DENYING
  5. 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.

JavaScript
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:

JavaScript
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.

Was this page helpful?