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

# Utility Modules for OpalScript

> Learn about how to use OpalScript's Utility Modules to automate access management workflows.

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

Utility modules provide common functionality available to all OpalScript types. These modules help you query Opal's data and make informed decisions.

## Query Opal's access graph

### accesslib Module

The `accesslib` module provides functions to query Opal's access graph. Use it to check existing permissions when making automation decisions.

`accesslib.check_access(principal_id, entity_id, [access_level_remote_id])` checks whether a principal (user or group) currently has access to an entity (resource or group).

<SectionHeader> Parameters </SectionHeader>

<ParamField path="principal_id" type="string (UUID)" required>
  The ID of the user or group to check
</ParamField>

<ParamField path="entity_id" type="string (UUID)" required>
  The ID of the resource or group to check access to
</ParamField>

<ParamField path="access_level_remote_id" type="string (UUID)">
  Filter by specific access level (e.g., `"admin"`, `"viewer"`)
</ParamField>

<SectionHeader> Returns </SectionHeader>
`True` if access exists, `False` otherwise

<SectionHeader> Examples </SectionHeader>

```python theme={null}
# Check if user has any access to a resource
has_access = accesslib.check_access(
    "550e8400-e29b-41d4-a716-446655440000",  # user ID
    "660e8400-e29b-41d4-a716-446655440001"   # resource ID
)

if has_access:
    print("User already has access")
```

```python theme={null}
# Check if user has a specific access level
is_admin = accesslib.check_access(
    user_id,
    resource_id,
    "admin"
)

if is_admin:
    print("User is an admin")
```

```python theme={null}
# Check group membership
in_engineering = accesslib.check_access(
    user_id,
    engineering_group_id
)
```

## Send notifications

### notificationslib Module

The `notificationslib` module provides functions to send notifications to users, admins, and owners from within a script.

***

### Notify a user

`notificationslib.notify_user(user_id, title, body)` sends a notification to a specific user via email and Slack (if configured).

<SectionHeader> Parameters </SectionHeader>

<ParamField path="user_id" type="string (UUID)" required>
  The ID of the user to notify
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body
</ParamField>

<SectionHeader> Returns </SectionHeader>
`True` if the notification was sent successfully, `False` otherwise

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
notificationslib.notify_user(
    request.requester_id,
    "Access request flagged",
    "Your request has been flagged for manual review."
)
```

***

### Notify all admins

`notificationslib.notify_admins(title, body)` sends a notification to all Opal admins.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body
</ParamField>

<SectionHeader> Returns </SectionHeader>
`True` if the notification was sent successfully, `False` otherwise

<SectionHeader> Example </SectionHeader>

```python theme={null}
notificationslib.notify_admins(
    "Unusual access request",
    "A request for admin access was auto-denied due to an anomalous pattern."
)
```

***

### Notify an owner

`notificationslib.notify_owner(owner_id, title, body)` sends a notification to an owner. If the owner has a Slack message channel configured, the notification is sent to that channel only. Otherwise it is sent to all individual users in the owner.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="owner_id" type="string (UUID)" required>
  The ID of the owner to notify
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body
</ParamField>

<SectionHeader> Returns </SectionHeader>
`True` if the notification was sent successfully, `False` otherwise

<SectionHeader> Example </SectionHeader>

```python theme={null}
notificationslib.notify_owner(
    "770e8400-e29b-41d4-a716-446655440002",
    "Request requires review",
    "A high-privilege access request has been submitted and needs manual approval."
)
```

## Manage support tickets

### ticketslib Module

The `ticketslib` module provides functions to create, retrieve, comment on, and close tickets in connected ticket providers. Use it to automatically file or update tickets as part of your access automation workflows.

Supported providers: **Jira**, **Linear**, **ServiceNow**, **Notion**, **FreshService**, and **Shortcut**. Only providers that are installed and connected to your Opal organization are available at runtime.

<Callout type="info">
  Tickets created via OpalScript are independent of tickets created via [ticket propagation](/docs/ticket-propagation#propagate-access-with-tickets). If ticket propagation is also enabled, a separate ticket will be created after the request is approved. Creating a ticket via OpalScript does not replace or affect that flow.
</Callout>

***

### Access a provider

`ticketslib.providers.<PROVIDER>` exposes the ticket providers installed for your organization as a namespace. Reference a provider by name to pass it to other `ticketslib` functions.

Available provider names: `JIRA`, `LINEAR`, `SERVICE_NOW`, `NOTION`, `FRESH_SERVICE`, `SHORTCUT`

<SectionHeader> Example </SectionHeader>

```python theme={null}
provider = ticketslib.providers.JIRA
```

***

### List projects

`ticketslib.list_projects(provider)` returns all projects available on the given provider.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="provider" type="ticket_provider" required>
  A provider from `ticketslib.providers`
</ParamField>

<SectionHeader> Returns </SectionHeader>

A dictionary keyed by project key (e.g. `"ENG"`). Each value is a `ticket_project` object:

| Attribute  | Type   | Description                                             |
| ---------- | ------ | ------------------------------------------------------- |
| `key`      | string | The project's key in the remote provider (e.g. `"ENG"`) |
| `name`     | string | The project's display name                              |
| `provider` | string | The provider name                                       |

<SectionHeader> Example </SectionHeader>

```python theme={null}
projects = ticketslib.list_projects(ticketslib.providers.JIRA)
eng_project = projects["ENG"]
```

***

### Create a ticket

`ticketslib.create_ticket(project, title, description)` creates a new ticket on the remote provider and stores it in Opal.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="project" type="ticket_project" required>
  A project object returned by `ticketslib.list_projects`
</ParamField>

<ParamField path="title" type="string" required>
  The title or summary of the ticket
</ParamField>

<ParamField path="description" type="string" required>
  The description or body of the ticket
</ParamField>

<SectionHeader> Returns </SectionHeader>

A `ticket` object:

| Attribute    | Type   | Description                                            |
| ------------ | ------ | ------------------------------------------------------ |
| `url`        | string | A URL to view the ticket in the provider's UI          |
| `identifier` | string | The human-readable ticket identifier (e.g. `"ENG-42"`) |
| `remote_id`  | string | The ticket's ID in the remote provider                 |
| `provider`   | string | The provider name                                      |
| `status`     | string | `"ACTIVE"`, `"CLOSED"`, or `"NOT_FOUND"`               |

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
projects = ticketslib.list_projects(ticketslib.providers.JIRA)
ticket = ticketslib.create_ticket(
    projects["ENG"],
    "Access request review: " + request.id,
    "Requester: " + request.requester_id + ". Reason: " + request.reason
)
actions.comment("Ticket filed for review: " + ticket.url)
```

***

### Get a ticket

`ticketslib.get_ticket(provider, remote_ticket_id)` fetches an existing ticket from the remote provider by its identifier.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="provider" type="ticket_provider" required>
  A provider from `ticketslib.providers`
</ParamField>

<ParamField path="remote_ticket_id" type="string" required>
  The ticket's identifier in the remote provider (e.g. `"ENG-42"`)
</ParamField>

<SectionHeader> Returns </SectionHeader>

A `ticket` object with the same attributes as returned by `create_ticket` above.

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
ticket_id = request.custom_fields.get("ticket_id", None)
if ticket_id:
    ticket = ticketslib.get_ticket(ticketslib.providers.JIRA, ticket_id)
    if ticket.status == "CLOSED":
        actions.approve("Associated ticket is resolved")
    else:
        actions.comment("Waiting on open ticket: " + ticket.url)
else:
    actions.comment("No ticket ID found in request")
```

***

### Comment on a ticket

`ticketslib.comment_ticket(provider, remote_ticket_id, comment)` adds a comment to an existing ticket. Does not change the ticket's status.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="provider" type="ticket_provider" required>
  A provider from `ticketslib.providers`
</ParamField>

<ParamField path="remote_ticket_id" type="string" required>
  The ticket's identifier in the remote provider (e.g. `"ENG-42"`)
</ParamField>

<ParamField path="comment" type="string" required>
  The text of the comment to add
</ParamField>

<SectionHeader> Returns </SectionHeader>
`None`

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
projects = ticketslib.list_projects(ticketslib.providers.JIRA)
ticket = ticketslib.create_ticket(
    projects["ENG"],
    "Access request: " + request.id,
    request.reason
)
ticketslib.comment_ticket(
    ticketslib.providers.JIRA,
    ticket.identifier,
    "Automatically created by Opal for access request " + request.id
)
actions.comment("Ticket created and annotated: " + ticket.url)
```

***

### Close a ticket

`ticketslib.close_ticket(provider, remote_ticket_id, [comment])` closes an existing ticket on the remote provider and updates its stored status in Opal to `CLOSED`. No-op if the ticket is already closed.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="provider" type="ticket_provider" required>
  A provider from `ticketslib.providers`
</ParamField>

<ParamField path="remote_ticket_id" type="string" required>
  The ticket's identifier in the remote provider (e.g. `"ENG-42"`)
</ParamField>

<ParamField path="comment" type="string">
  An optional closing comment to add before closing
</ParamField>

<SectionHeader> Returns </SectionHeader>
`None`

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
ticket_id = request.custom_fields.get("ticket_id", None)
if ticket_id:
    ticketslib.close_ticket(
        ticketslib.providers.JIRA,
        ticket_id,
        "Request denied by Opal automation."
    )
    actions.deny("Request denied and associated ticket closed.")
else:
    actions.deny("Request denied.")
```

<SectionHeader> End-to-end example </SectionHeader>

```python theme={null}
# Create a ticket for high-risk requests, annotate it, then deny with a link
request = context.get_request()
resource = entitylib.get_resource(request.requested_resources[0].resource_id)
sensitivity = assetlib.get_resource_risk_sensitivity(resource.id)

if sensitivity in ["CRITICAL", "HIGH"]:
    projects = ticketslib.list_projects(ticketslib.providers.JIRA)
    ticket = ticketslib.create_ticket(
        projects["SEC"],
        "High-risk access request: " + resource.name,
        "Request " + request.id + " for '" + resource.name + "' was auto-denied. Risk level: " + sensitivity
    )
    ticketslib.comment_ticket(
        ticketslib.providers.JIRA,
        ticket.identifier,
        "Requester: " + request.requester_id + ". Reason: " + request.reason
    )
    actions.deny("High-risk resource requires manual review. Ticket filed: " + ticket.url)
else:
    actions.approve("Resource risk level acceptable")
```

## Query assets

### assetlib Module

The `assetlib` module provides functions to look up the risk sensitivity classification of resources and groups, and to query historical access requests for them.

Request history lookups return a dictionary keyed by stringified request UUID. The values are request objects with the same shape as the one returned by [`context.get_request()`](/docs/requestreview-getstarted#request-object). If no requests match, the dictionary is empty. Looking up a missing ID raises a key error, so guard with `if request_id in requests:` when needed. Pass empty strings (`""`) to skip any optional string argument.

Results are ordered by `(updated_at DESC, id DESC)` and filtered to the caller's organization. The returned value also exposes a `next_cursor` attribute — an opaque string to pass back as the `cursor` argument on the next call. It is `""` when there are no further pages. When both `cursor` and `requests_per_page` are omitted, every match is returned in a single call and `next_cursor` is `""`.

```python theme={null}
page = assetlib.get_requests_for_resource(resource_id, "", "", "", 50)
page.next_cursor  # string — empty when no more pages
```

***

### Get resource risk sensitivity

`assetlib.get_resource_risk_sensitivity(resource_id)` returns the risk sensitivity level of a resource.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="resource_id" type="string (UUID)" required>
  The ID of the resource
</ParamField>

<SectionHeader> Returns </SectionHeader>

A string representing the risk level: `"UNKNOWN"`, `"NONE"`, `"LOW"`, `"MEDIUM"`, `"HIGH"`, or `"CRITICAL"`

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
resource_id = request.requested_resources[0]
sensitivity = assetlib.get_resource_risk_sensitivity(resource_id)

if sensitivity == "CRITICAL":
    actions.deny("Cannot auto-approve access to critical resources")
elif sensitivity == "HIGH":
    actions.comment("High-risk resource - flagged for manual review")
else:
    actions.approve("Low-risk resource")
```

***

### Get group risk sensitivity

`assetlib.get_group_risk_sensitivity(group_id)` returns the risk sensitivity level of a group.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="group_id" type="string (UUID)" required>
  The ID of the group
</ParamField>

<SectionHeader> Returns </SectionHeader>

A string representing the risk level: `"UNKNOWN"`, `"NONE"`, `"LOW"`, `"MEDIUM"`, `"HIGH"`, or `"CRITICAL"`

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
group_sensitivity = assetlib.get_group_risk_sensitivity(request.target_group_id)

if group_sensitivity in ["CRITICAL", "HIGH"]:
    actions.comment("This group contains high-risk assets - manual review required")
else:
    actions.approve("Group does not contain critical assets")
```

***

### Get requests for a resource

`assetlib.get_requests_for_resource(resource_id, [user_id], [request_status], [cursor], [requests_per_page])` returns every request whose `requested_resources` includes `resource_id`. The optional `user_id` filter narrows to requests **submitted by** that user (`requester_id`), not the target user.

A single request that asked for multiple resources will appear in queries for each one.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="resource_id" type="string (UUID)" required>
  The resource being queried
</ParamField>

<ParamField path="user_id" type="string (UUID)">
  Narrow to requests submitted by this user. Pass `""` to skip
</ParamField>

<ParamField path="request_status" type="string">
  One of `"PENDING"`, `"APPROVED"`, `"DENIED"`, `"CANCELED"`. Pass `""` to skip
</ParamField>

<ParamField path="cursor" type="string">
  Opaque page cursor from a previous call's `next_cursor`. Pass `""` for the first page
</ParamField>

<ParamField path="requests_per_page" type="int">
  Page size. Must be positive. Omit to return all matches in a single call
</ParamField>

<SectionHeader> Returns </SectionHeader>

A dictionary keyed by request UUID, with a `next_cursor` attribute for pagination. See [Request object](/docs/requestreview-getstarted#request-object) for the value shape.

<SectionHeader> Examples </SectionHeader>

```python theme={null}
# How many times has anyone been granted access to this resource?
request = context.get_request()
resource_id = request.requested_resources[0].resource_id
approved = assetlib.get_requests_for_resource(resource_id, "", "APPROVED")
actions.comment("Approved " + str(len(approved)) + " times previously")
```

```python theme={null}
# Has the requester ever been denied this resource before?
request = context.get_request()
resource_id = request.requested_resources[0].resource_id
prior_denials = assetlib.get_requests_for_resource(
    resource_id,
    request.requester_id,
    "DENIED",
)
if len(prior_denials) > 0:
    actions.comment("Requester previously denied this resource")
```

```python theme={null}
# Walk all approved requests one page at a time
request = context.get_request()
resource_id = request.requested_resources[0].resource_id
cursor = ""
for _ in range(100):
    page = assetlib.get_requests_for_resource(resource_id, "", "APPROVED", cursor, 50)
    for request_id, prior_request in page.items():
        process(prior_request)
    if page.next_cursor == "":
        break
    cursor = page.next_cursor
```

***

### Get requests for a group

`assetlib.get_requests_for_group(group_id, [user_id], [request_status], [cursor], [requests_per_page])` returns every request whose `requested_groups` includes `group_id`. This filters on **what was asked for** — a request with `target_group_id = X` but no `X` in `requested_groups` does not match.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="group_id" type="string (UUID)" required>
  The group being queried
</ParamField>

<ParamField path="user_id" type="string (UUID)">
  Narrow to requests submitted by this user. Pass `""` to skip
</ParamField>

<ParamField path="request_status" type="string">
  One of `"PENDING"`, `"APPROVED"`, `"DENIED"`, `"CANCELED"`. Pass `""` to skip
</ParamField>

<ParamField path="cursor" type="string">
  Opaque page cursor from a previous call's `next_cursor`. Pass `""` for the first page
</ParamField>

<ParamField path="requests_per_page" type="int">
  Page size. Must be positive. Omit to return all matches in a single call
</ParamField>

<SectionHeader> Returns </SectionHeader>

A dictionary keyed by request UUID, with a `next_cursor` attribute for pagination. See [Request object](/docs/requestreview-getstarted#request-object) for the value shape.

<SectionHeader> Example </SectionHeader>

```python theme={null}
# Pending requests for this group from the same requester
request = context.get_request()
overlapping = 0
for requested_group in request.requested_groups:
    prior = assetlib.get_requests_for_group(
        requested_group.group_id,
        request.requester_id,
        "PENDING",
    )
    if len(prior) > 1:
        overlapping += 1
if overlapping > 0:
    actions.comment("Requester has overlapping pending requests for " + str(overlapping) + " groups")
```

## Query user request history

### userlib Module

The `userlib` module provides functions to query Opal's request history for a user.

`userlib.get_requests(user_id, [request_status], [cursor], [requests_per_page])` returns every request where the given user is either the **requester** (submitted it) or the **target user** (recipient of the access). A self-request (requester == target) appears once.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="user_id" type="string (UUID)" required>
  The ID of the user
</ParamField>

<ParamField path="request_status" type="string">
  One of `"PENDING"`, `"APPROVED"`, `"DENIED"`, `"CANCELED"`. Pass `""` to skip
</ParamField>

<ParamField path="cursor" type="string">
  Opaque page cursor from a previous call's `next_cursor`. Pass `""` for the first page
</ParamField>

<ParamField path="requests_per_page" type="int">
  Page size. Must be positive. Omit to return all matches in a single call
</ParamField>

<SectionHeader> Returns </SectionHeader>

A dictionary keyed by request UUID, with a `next_cursor` attribute for pagination. See [Request object](/docs/requestreview-getstarted#request-object) for the value shape. Looking up a missing ID raises a key error — guard with `if request_id in requests:` when needed.

<SectionHeader> Examples </SectionHeader>

```python theme={null}
# All approved requests this user has been involved in
request = context.get_request()
prior = userlib.get_requests(request.requester_id, "APPROVED")

actions.comment("User has " + str(len(prior)) + " prior approved requests")
```

```python theme={null}
# Walk every request for this user one page at a time
cursor = ""
for _ in range(100):
    page = userlib.get_requests(user_id, "", cursor, 50)
    for request_id, prior_request in page.items():
        process(prior_request)
    if page.next_cursor == "":
        break
    cursor = page.next_cursor
```

## Work with time

### timelib Module

The `timelib` module provides functions to work with Unix timestamps (seconds since epoch) and time intervals for access duration validation and temporal logic.

***

### Get current time

`timelib.now()` returns the current Unix timestamp (seconds since epoch).

<SectionHeader> Returns </SectionHeader>

An integer representing the current time as a Unix timestamp

<SectionHeader> Example </SectionHeader>

```python theme={null}
current_time = timelib.now()
actions.comment("Request processed at " + timelib.from_unix(current_time))
```

***

### Convert timestamp to string

`timelib.from_unix(timestamp)` converts a Unix timestamp to an RFC3339 formatted string in UTC (e.g., `2024-01-15T10:30:45Z`).

<SectionHeader> Parameters </SectionHeader>

<ParamField path="timestamp" type="int" required>
  Unix timestamp to convert
</ParamField>

<SectionHeader> Returns </SectionHeader>

A string in RFC3339 format

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
request_time_str = timelib.from_unix(request.created_at)
actions.comment("Request was created at " + request_time_str)
```

***

### Compare timestamps

`timelib.is_before(timestamp1, timestamp2)` checks if the first timestamp is before the second.

`timelib.is_after(timestamp1, timestamp2)` checks if the first timestamp is after the second.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="timestamp1" type="int" required>
  First timestamp
</ParamField>

<ParamField path="timestamp2" type="int" required>
  Second timestamp
</ParamField>

<SectionHeader> Returns </SectionHeader>

`True` or `False`

<SectionHeader> Example </SectionHeader>

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

# Deny requests older than 30 days
if timelib.is_before(request.created_at, now - timelib.days(30)):
    actions.deny("Request has expired (older than 30 days)")

# Auto-approve short-duration recent requests
if timelib.is_after(request.created_at, now - timelib.days(7)):
    if request.requested_duration_minutes <= 60:
        actions.approve("Recent short-duration request")
```

***

### Calculate time differences

`timelib.seconds_since(timestamp1, timestamp2)` returns the number of seconds between two timestamps (positive if timestamp1 is after timestamp2).

<SectionHeader> Parameters </SectionHeader>

<ParamField path="timestamp1" type="int" required>
  First timestamp
</ParamField>

<ParamField path="timestamp2" type="int" required>
  Second timestamp
</ParamField>

<SectionHeader> Returns </SectionHeader>

An integer representing the difference in seconds

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
age_seconds = timelib.seconds_since(timelib.now(), request.created_at)
actions.comment("Request is " + str(age_seconds) + " seconds old")
```

***

### Convert time intervals to seconds

Use `timelib.minutes(n)`, `timelib.hours(n)`, and `timelib.days(n)` to convert human-readable time intervals to seconds. This is useful for time comparisons and avoids hardcoding magic numbers.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="n" type="int" required>
  Number of time units
</ParamField>

<SectionHeader> Returns </SectionHeader>

An integer representing the number of seconds

<SectionHeader> Examples </SectionHeader>

```python theme={null}
# Convert time intervals to seconds
five_minutes = timelib.minutes(5)      # Returns 300
two_hours = timelib.hours(2)           # Returns 7200
thirty_days = timelib.days(30)         # Returns 2592000
```

```python theme={null}
# Use in time comparisons
request = context.get_request()
deadline = request.created_at + timelib.days(7)

if timelib.is_after(timelib.now(), deadline):
    actions.deny("Request expired after 7 days")
else:
    actions.approve("Request is within the 7-day window")
```

```python theme={null}
# Check request duration
if request.requested_duration_minutes < 5:
    actions.approve("Very short-duration request")
elif request.requested_duration_minutes <= timelib.hours(24) // 60:
    actions.comment("Duration is 24 hours or less - acceptable")
else:
    actions.comment("Duration exceeds 24 hours - requires review")
```

***

## Important notes

* **Timestamps are UTC only**: All `timelib` functions work in UTC. There is no timezone conversion support.
* **Second precision**: Timestamps have second-level precision. Sub-second differences are not available.
* **No test mode**: OpalScript automations execute on real access requests. Test your logic with low-risk parameters first.
* **Review before deploying**: All OpalScript automations should be reviewed by a human before deployment to catch logic errors.

## Look up entity information

### entitylib Module

The `entitylib` module provides functions to look up users, groups, and resources by their IDs. Use it to access entity properties and tags when making automation decisions.

***

### Get a user

`entitylib.get_user(user_id)` retrieves a user by their UUID.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="user_id" type="string (UUID)" required>
  The ID of the user to fetch
</ParamField>

<SectionHeader> Returns </SectionHeader>

| Attribute         | Type           | Description                                 |
| ----------------- | -------------- | ------------------------------------------- |
| `id`              | string         | The user's UUID                             |
| `position`        | string         | The user's job position                     |
| `team`            | string or None | The user's team name                        |
| `manager_id`      | string or None | The UUID of the user's manager              |
| `is_service_user` | bool           | Whether the user is a service user          |
| `is_deleted`      | bool           | Whether the user is deleted                 |
| `tags`            | dict           | Tags assigned to the user, keyed by tag key |

<SectionHeader> Examples </SectionHeader>

```python theme={null}
request = context.get_request()
user = entitylib.get_user(request.requester_id)
department = user.tags.get("department", None)
```

```python theme={null}
# Look up a user's manager
request = context.get_request()
user = entitylib.get_user(request.requester_id)
if user.manager_id:
    manager = entitylib.get_user(user.manager_id)
    notificationslib.notify_user(manager.id, "Access request pending", "A member of your team has submitted an access request.")
```

***

### Get a group

`entitylib.get_group(group_id)` retrieves a group by its UUID.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="group_id" type="string (UUID)" required>
  The ID of the group to fetch
</ParamField>

<SectionHeader> Returns </SectionHeader>

| Attribute     | Type   | Description                                  |
| ------------- | ------ | -------------------------------------------- |
| `id`          | string | The group's UUID                             |
| `name`        | string | The group's name                             |
| `description` | string | The group's description                      |
| `group_type`  | string | The type of group (e.g., `"STANDARD"`)       |
| `is_deleted`  | bool   | Whether the group is deleted                 |
| `tags`        | dict   | Tags assigned to the group, keyed by tag key |

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
for requested_group in request.requested_groups:
    group = entitylib.get_group(requested_group.group_id)
    cost_center = group.tags.get("cost_center", None)
```

***

### Get a resource

`entitylib.get_resource(resource_id)` retrieves a resource by its UUID.

<SectionHeader> Parameters </SectionHeader>

<ParamField path="resource_id" type="string (UUID)" required>
  The ID of the resource to fetch
</ParamField>

<SectionHeader> Returns </SectionHeader>

| Attribute       | Type   | Description                                     |
| --------------- | ------ | ----------------------------------------------- |
| `id`            | string | The resource's UUID                             |
| `name`          | string | The resource's name                             |
| `description`   | string | The resource's description                      |
| `resource_type` | string | The type of resource (e.g., `"GITHUB"`)         |
| `is_deleted`    | bool   | Whether the resource is deleted                 |
| `tags`          | dict   | Tags assigned to the resource, keyed by tag key |

<SectionHeader> Example </SectionHeader>

```python theme={null}
request = context.get_request()
for requested_resource in request.requested_resources:
    resource = entitylib.get_resource(requested_resource.resource_id)
    env = resource.tags.get("env", None)
    if env == "production":
        actions.deny("Production access requires manual review.")
```

***

### Tags

All entity objects include a `tags` dictionary that maps tag keys to string values (`None` if the tag has no value).

```python theme={null}
request = context.get_request()
user = entitylib.get_user(request.requester_id)

# Safe access with default
department = user.tags.get("department", "Unknown")

# Check if a tag exists before accessing
if "department" in user.tags:
    actions.comment("Department: " + user.tags["department"])
```
