Skip to main content

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 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).
principal_id
string (UUID)
required
The ID of the user or group to check
entity_id
string (UUID)
required
The ID of the resource or group to check access to
access_level_remote_id
string (UUID)
Filter by specific access level (e.g., "admin", "viewer")
True if access exists, False otherwise
# 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")
# 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")
# 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).
user_id
string (UUID)
required
The ID of the user to notify
title
string
required
Notification title
body
string
required
Notification body
True if the notification was sent successfully, False otherwise
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.
title
string
required
Notification title
body
string
required
Notification body
True if the notification was sent successfully, False otherwise
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.
owner_id
string (UUID)
required
The ID of the owner to notify
title
string
required
Notification title
body
string
required
Notification body
True if the notification was sent successfully, False otherwise
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.
Tickets created via OpalScript are independent of tickets created via ticket propagation. 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.

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
provider = ticketslib.providers.JIRA

List projects

ticketslib.list_projects(provider) returns all projects available on the given provider.
provider
ticket_provider
required
A provider from ticketslib.providers
A dictionary keyed by project key (e.g. "ENG"). Each value is a ticket_project object:
AttributeTypeDescription
keystringThe project’s key in the remote provider (e.g. "ENG")
namestringThe project’s display name
providerstringThe provider name
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.
project
ticket_project
required
A project object returned by ticketslib.list_projects
title
string
required
The title or summary of the ticket
description
string
required
The description or body of the ticket
A ticket object:
AttributeTypeDescription
urlstringA URL to view the ticket in the provider’s UI
identifierstringThe human-readable ticket identifier (e.g. "ENG-42")
remote_idstringThe ticket’s ID in the remote provider
providerstringThe provider name
statusstring"ACTIVE", "CLOSED", or "NOT_FOUND"
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.
provider
ticket_provider
required
A provider from ticketslib.providers
remote_ticket_id
string
required
The ticket’s identifier in the remote provider (e.g. "ENG-42")
A ticket object with the same attributes as returned by create_ticket above.
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.
provider
ticket_provider
required
A provider from ticketslib.providers
remote_ticket_id
string
required
The ticket’s identifier in the remote provider (e.g. "ENG-42")
comment
string
required
The text of the comment to add
None
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.
provider
ticket_provider
required
A provider from ticketslib.providers
remote_ticket_id
string
required
The ticket’s identifier in the remote provider (e.g. "ENG-42")
comment
string
An optional closing comment to add before closing
None
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.")
# 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 asset risk sensitivity

assetlib Module

The assetlib module provides functions to check the risk sensitivity classification of resources and groups based on their connection type or admin overrides.

Get resource risk sensitivity

assetlib.get_resource_risk_sensitivity(resource_id) returns the risk sensitivity level of a resource.
resource_id
string (UUID)
required
The ID of the resource
A string representing the risk level: "UNKNOWN", "NONE", "LOW", "MEDIUM", "HIGH", or "CRITICAL"
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.
group_id
string (UUID)
required
The ID of the group
A string representing the risk level: "UNKNOWN", "NONE", "LOW", "MEDIUM", "HIGH", or "CRITICAL"
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")

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). An integer representing the current time as a Unix timestamp
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).
timestamp
int
required
Unix timestamp to convert
A string in RFC3339 format
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.
timestamp1
int
required
First timestamp
timestamp2
int
required
Second timestamp
True or False
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).
timestamp1
int
required
First timestamp
timestamp2
int
required
Second timestamp
An integer representing the difference in seconds
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.
n
int
required
Number of time units
An integer representing the number of seconds
# 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
# 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")
# 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.
user_id
string (UUID)
required
The ID of the user to fetch
AttributeTypeDescription
idstringThe user’s UUID
positionstringThe user’s job position
teamstring or NoneThe user’s team name
manager_idstring or NoneThe UUID of the user’s manager
is_service_userboolWhether the user is a service user
is_deletedboolWhether the user is deleted
tagsdictTags assigned to the user, keyed by tag key
request = context.get_request()
user = entitylib.get_user(request.requester_id)
department = user.tags.get("department", None)
# 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.
group_id
string (UUID)
required
The ID of the group to fetch
AttributeTypeDescription
idstringThe group’s UUID
namestringThe group’s name
descriptionstringThe group’s description
group_typestringThe type of group (e.g., "STANDARD")
is_deletedboolWhether the group is deleted
tagsdictTags assigned to the group, keyed by tag key
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.
resource_id
string (UUID)
required
The ID of the resource to fetch
AttributeTypeDescription
idstringThe resource’s UUID
namestringThe resource’s name
descriptionstringThe resource’s description
resource_typestringThe type of resource (e.g., "GITHUB")
is_deletedboolWhether the resource is deleted
tagsdictTags assigned to the resource, keyed by tag key
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).
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"])
Last modified on May 7, 2026