> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opal.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Start automating workflows around requests

> Learn how to create request review scripts in OpalScript.

export const SectionHeader = ({children}) => {
  return <div style={{
    fontWeight: "bold",
    borderBottom: "1px solid #e5e7eb",
    paddingBottom: "0.25rem",
    marginBottom: "0.5rem"
  }}>
      {children}
    </div>;
};

<Info>
  Each script type has its own `context` and `actions` modules tailored to that
  automation scenario.
</Info>

## Overview

Request Review scripts run when an access request is assigned to a service user for review. The script can automatically approve, deny, or add comments to the request.

This guide details the context and action modules you can use to get started, as well as the best practices and limitations when using OpalScript. For use case specific sample scripts, see our [examples](/docs/requestreview-examples).

## Quick Start

The simplest script approves all requests:

```python theme={null}
actions.approve()
```

A more practical script evaluates the request:

```python theme={null}
request = context.get_request()

if "urgent" in request.reason.lower():
    actions.approve("Auto-approved: urgent request")
else:
    actions.comment("Flagged for manual review")
```

***

## context Module

The `context` module provides read-only access to the request being reviewed.

### Get the request object

`context.get_request()` returns the [Request object](#request-object) being reviewed.

<SectionHeader> Parameters </SectionHeader>

None

<SectionHeader>Returns</SectionHeader>

<ResponseField name="Request" type="object">
  Contains information about the access request

  <Expandable title="properties">
    <ResponseField name="id" type="string (UUID)">
      Unique identifier for the request
    </ResponseField>

    <ResponseField name="reason" type="string">
      The reason provided by the requester
    </ResponseField>

    <ResponseField name="requester_id" type="string (UUID)">
      ID of the user who created the request
    </ResponseField>

    <ResponseField name="target_user_id" type="string">
      ID of the user being granted access if applicable. If not applicable, then{" "}
      <Badge size="sm"> None </Badge>
    </ResponseField>

    <ResponseField name="target_group_id" type="string">
      ID of the group being granted access if applicable. If not applicable,
      then <Badge size="sm"> None </Badge>
    </ResponseField>

    <ResponseField name="requested_duration_minutes" type="int">
      Requested access duration in minutes, if not applicable then
      <Badge size="sm"> None </Badge>
    </ResponseField>

    <ResponseField name="status" type="string">
      Current status (e.g., `"PENDING"`, `"APPROVED"`, `"DENIED"`)
    </ResponseField>

    <ResponseField name="requested_resources" type="list[RequestedResource]">
      list of the [RequestedResource object](#requestedresource-Object) , which
      are the resources included in this request
    </ResponseField>

    <ResponseField name="requested_groups" type="list[RequestedGroup]">
      list of the [RequestedGroups object](#requestedgroup-object), which are
      the groups included in this request
    </ResponseField>

    <ResponseField name="custom_fields" type="Dict[String, value]">
      Custom field values from the request template
    </ResponseField>
  </Expandable>
</ResponseField>

<SectionHeader>Example</SectionHeader>

```python theme={null}
request = context.get_request()
print(request.reason)
print(request.requester_id)
```

***

## actions Module

The `actions` module provides methods to take action on the request.

### Approve a request

`actions.approve([comment])` approves the request.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="comment" type="string">
  Optional comment
</ParamField>

<SectionHeader>Example</SectionHeader>

```python theme={null}
actions.approve()
actions.approve("Auto-approved: meets all criteria")
```

### Deny a request

`actions.deny(comment)` denies a request. A comment is required to explain the denial.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="comment" type="string" required>
  Comment to add
</ParamField>

<SectionHeader>Example</SectionHeader>

```python theme={null}
actions.comment("Flagged: unusual access pattern detected")
```

### Comment on a request

`actions.comment(comment)` adds a comment without changing the request status. Useful for flagging requests or adding context for manual reviewers.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="comment" type="string" required>
  Comment to add
</ParamField>

<SectionHeader>Example</SectionHeader>

```python theme={null}
actions.comment("Flagged: unusual access pattern detected")
```

***

## Objects

### Request object

Returned by `context.get_request()` and contains information about the access request.

| Attribute                    | Type                                                  | Description                                                  |
| ---------------------------- | ----------------------------------------------------- | ------------------------------------------------------------ |
| `id`                         | String (UUID)                                         | Unique identifier for the request                            |
| `reason`                     | String                                                | The reason provided by the requester                         |
| `requester_id`               | String (UUID)                                         | ID of the user who created the request                       |
| `target_user_id`             | String or None                                        | ID of the user being granted access (if applicable)          |
| `target_group_id`            | String or None                                        | ID of the group being granted access (if applicable)         |
| `requested_duration_minutes` | int or None                                           | Requested access duration in minutes                         |
| `status`                     | String                                                | Current status (e.g., `"PENDING"`, `"APPROVED"`, `"DENIED"`) |
| `requested_resources`        | List\[[RequestedResource](#requestedresource-object)] | Resources included in this request                           |
| `requested_groups`           | List\[[RequestedGroup](#requestedgroup-object)]       | Groups included in this request                              |
| `custom_fields`              | Dict\[String, value]                                  | Custom field values from the request template                |

<SectionHeader> Example Usage </SectionHeader>

```python theme={null}
request = context.get_request()

# Basic properties
reason = request.reason
requester = request.requester_id

# Check optional properties
if request.requested_duration_minutes:
    duration = request.requested_duration_minutes

# Iterate over requested resources
for resource in request.requested_resources:
    print(resource.resource_name)

# Access custom fields
ticket_number = request.custom_fields.get("ticket_number", "")
```

### RequestedResource object

Represents a resource included in the request. Returned in the Request object.

| Attribute                | Type           | Description                                         |
| ------------------------ | -------------- | --------------------------------------------------- |
| `id`                     | String (UUID)  | Unique identifier for this requested resource entry |
| `resource_id`            | String (UUID)  | ID of the resource being requested                  |
| `resource_name`          | String or None | Name of the resource                                |
| `resource_type`          | String or None | Type of the resource (e.g., `"AWS_IAM_ROLE"`)       |
| `access_level_name`      | String         | Display name of the requested access level          |
| `access_level_remote_id` | String         | Remote identifier for the access level              |

<SectionHeader> Example Usage </SectionHeader>

```python theme={null}
for resource in request.requested_resources:
    print(f"Resource: {resource.resource_name}")
    print(f"Type: {resource.resource_type}")
    print(f"Access Level: {resource.access_level_name}")
```

### RequestedGroup object

Represents a group included in the request. Returned in the Request object.

| Attribute                | Type           | Description                                      |
| ------------------------ | -------------- | ------------------------------------------------ |
| `id`                     | String (UUID)  | Unique identifier for this requested group entry |
| `group_id`               | String (UUID)  | ID of the group being requested                  |
| `group_name`             | String or None | Name of the group                                |
| `group_type`             | String or None | Type of the group (e.g., `"OKTA_GROUP"`)         |
| `access_level_name`      | String         | Display name of the requested access level       |
| `access_level_remote_id` | String         | Remote identifier for the access level           |

<SectionHeader> Example Usage </SectionHeader>

```python theme={null}
for group in request.requested_groups:
    print(f"Group: {group.group_name}")
    print(f"Type: {group.group_type}")
```

### Custom Fields

The `custom_fields` attribute is a dictionary containing values from the request template's custom fields. The keys are field names, and values depend on the field type:

| Field Type   | Value Type | Example                       |
| ------------ | ---------- | ----------------------------- |
| Short Text   | String     | `"JIRA-1234"`                 |
| Long Text    | String     | `"Detailed justification..."` |
| Boolean      | bool       | `True`                        |
| Multi-Choice | String     | `"Option A"`                  |

<SectionHeader> Example </SectionHeader>

```python theme={null}
custom_fields = request.custom_fields

# Access with default value
ticket = custom_fields.get("ticket_number", "")
is_emergency = custom_fields.get("emergency_access", False)

# Check if field exists
if "justification" in custom_fields:
    justification = custom_fields["justification"]
```

## Constraints & Limits

OpalScript enforces limits to ensure safe, predictable execution:

| Constraint          | Limit      | Description                                  |
| ------------------- | ---------- | -------------------------------------------- |
| **Script Size**     | 100 KB     | Maximum script length                        |
| **Execution Time**  | 30 seconds | Scripts timeout after 30 seconds             |
| **Execution Steps** | 1,000,000  | Maximum operations to prevent infinite loops |

### Unsupported Operations

For security and reliability, OpalScript does not support:

* **External HTTP calls**: Scripts cannot make network requests
* **File I/O**: Scripts cannot read or write files
* **Direct database access**: All data access goes through provided modules
* **Import statements**: All modules are pre-loaded
* **While loops**: Use `for` loops with `range()` instead

## Error Handling

Scripts can fail due to various errors. Understanding common error types helps you write more robust scripts.

### Syntax Errors

```python theme={null}
# Missing colon
if True
    actions.approve()  # Error: missing colon
```

### None Value Errors

```python theme={null}
request = context.get_request()

# Unsafe: duration might be None
if request.requested_duration_minutes < 60:  # Error if None
    actions.approve()

# Safe: check for None first
if request.requested_duration_minutes and request.requested_duration_minutes < 60:
    actions.approve()
```

### Type Errors

```python theme={null}
# Error: concatenating string and int
message = "Count: " + 5

# Correct: convert to string
message = "Count: " + str(5)
```

## Best Practices

1. **Check for None** before using optional attributes
2. **Use `.get()` with defaults** when accessing dictionary values
3. **Keep scripts focused** - do one thing well
4. **Test with edge cases** - empty strings, None values, missing fields

```python theme={null}
# Defensive coding example
request = context.get_request()

# Safe dictionary access
custom_fields = request.custom_fields
ticket = custom_fields.get("ticket_number", "")

# Safe optional attribute access
duration = request.requested_duration_minutes
if duration is not None and duration <= 240:
    actions.approve("Short duration approved")
elif duration is None:
    actions.comment("No duration specified")
else:
    actions.comment("Duration requires review")
```
