Introduction
In Week 1, we rebooted the Rubrik PHP framework and defined a 12-week roadmap.
In Week 2, we explored the GraphQL schema and the Connection -> Node pattern that underpins the RSC API.
In Week 3, we built rkRscGetToken(), a production-ready OAuth2 authentication helper with token caching and environment-variable-based secret management.
In Week 4, we connected all the pieces and executed our first real GraphQL query from PHP. We introduced rkGetSLAs() as a minimal working implementation: id and name, no pagination, no retention detail.
We closed that post with a clear warning: "Working code is not the same as robust code."
This week, we make good on that promise.
We are extending rkGetSLAs() into a production-ready data access function by adding cursor-based pagination, normalized retention data, and clean CLI output. By the end of this post, the framework can retrieve the full SLA inventory from any RSC environment regardless of size, and expose it in a format that is immediately useful for reporting, compliance validation, and automation.
Why SLA Domains Are a Foundation Workload
SLA Domains are the central policy object in Rubrik Security Cloud. Every protected workload (VMware VMs, filesets, cloud workloads, databases) derives its protection behaviour from an SLA Domain assignment. The domain defines:
- Frequency and retention duration
- Local, replica, and archive targets
- Protection policy enforcement logic
Being able to retrieve and normalize this data programmatically is a prerequisite for almost every operational workflow we will build later in this series: compliance reporting, inventory analysis, protection gap detection, and audit trails.
Even a basic SLA inventory immediately answers practical questions:
- Which retention policies currently exist in the environment?
- Are deprecated SLA Domains still present and potentially still assigned to workloads?
- Are naming conventions being respected across teams?
- Does the current SLA configuration align with internal protection standards?
These are not edge cases. They are day-one operational questions for any backup engineer managing RSC at scale.
The Query: SLA Domains with Pagination and Retention
In Week 4, we used the minimal slaDomains query: id and name only. Production environments require more.
We need retention data, and we need pagination to handle environments with large SLA inventories. Here is the extended query:
query ListSlas($first: Int!, $after: String) {
slaDomains(first: $first, after: $after) {
nodes {
id
name
... on GlobalSlaReply {
localRetentionLimit {
duration
unit
}
}
... on ClusterSlaDomain {
localRetentionLimit {
duration
unit
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}A few things worth understanding before writing PHP against this:
- nodes contains the SLA objects, following the same Connection -> Node pattern from Week 2.
- pageInfo drives pagination: hasNextPage signals whether more data exists, and endCursor is the cursor to pass on the next call.
- Inline fragments (... on) are required because SLA Domains are a union type. GlobalSlaReply and ClusterSlaDomain are the two concrete implementations. Both expose localRetentionLimit, but they are distinct types and GraphQL requires you to be explicit.
- localRetentionLimit returns duration (integer) and unit (string, e.g. DAYS).
If your environment still exposes slaDomainConnection, the structure is identical; only the field name changes.
Practitioner Tip "When you encounter a union type in the RSC schema, the Playground immediately shows which concrete types implement it and what fields are available on each. Use it before writing inline fragments. It tells you exactly what to request and avoids silent misses on fields that exist only on one type." |
Handling Pagination Correctly
This is the first function in the framework that implements cursor-based pagination in code. The pattern is straightforward, but it must be built from the start. Retrofitting it later means every caller that assumed a full dataset was silently working with incomplete data.
The loop logic:
- Initialize $after = null
- Execute the query with first: 100 and the current $after value
- Append nodes to the result array
- Read hasNextPage and endCursor from pageInfo
- If hasNextPage is true, advance $after to endCursor and repeat
- If false, exit
Practitioner Tip "Always implement pagination from day one, even when you know the current dataset is small. APIs evolve, environments grow, and a function that silently truncates results at page one is worse than one that fails loudly. Silent data loss in a compliance report is exactly the class of bug you cannot afford to discover during an audit." |
Normalizing Retention Data
The API returns retention as a structured object:
{
"duration": 365,
"unit": "DAYS"
}Inside the framework, we normalize this into a single readable string: 365 DAYS. If no retention data is present (which can occur for SLA Domains without a defined local retention limit), we fall back to N/A.
Every consumer of rkGetSLAs() (reporting scripts, compliance checks, CLI tools) gets a consistent, predictable string rather than a nullable nested object. That consistency is what makes the function trustworthy as a data source across the framework.
Implementing rkGetSLAs()
Here is the full production implementation:
<?php
function rkGetSLAs(string $accessToken): array
{
$allSlas = [];
$after = null;
do
{
// ----------------------------------------
// Build query with pagination variables
// ----------------------------------------
$query = <<<'GQL'
query ListSlas($first: Int!, $after: String) {
slaDomains(first: $first, after: $after) {
nodes {
id
name
... on GlobalSlaReply {
localRetentionLimit {
duration
unit
}
}
... on ClusterSlaDomain {
localRetentionLimit {
duration
unit
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
GQL;
$variables = [
'first' => 100,
'after' => $after,
];
// ----------------------------------------
// Execute via shared GraphQL helper
// ----------------------------------------
$response = rkCallGraphQL($accessToken, $query, $variables);
// ----------------------------------------
// Validate response structure
// ----------------------------------------
if (!isset($response['data']['slaDomains']['nodes']))
{
throw new RuntimeException(
'Unexpected GraphQL response structure in rkGetSLAs().'
);
}
$nodes = $response['data']['slaDomains']['nodes'];
$pageInfo = $response['data']['slaDomains']['pageInfo'];
// ----------------------------------------
// Normalize each SLA Domain
// ----------------------------------------
foreach ($nodes as $node)
{
$retention = 'N/A';
if (
isset(
$node['localRetentionLimit']['duration'],
$node['localRetentionLimit']['unit']
)
)
{
$retention = $node['localRetentionLimit']['duration']
. ' '
. $node['localRetentionLimit']['unit'];
}
$allSlas[] = [
'id' => $node['id'] ?? '',
'name' => $node['name'] ?? '',
'retention' => $retention,
];
}
// ----------------------------------------
// Advance cursor or exit loop
// ----------------------------------------
$after = ($pageInfo['hasNextPage'] ?? false)
? ($pageInfo['endCursor'] ?? null)
: null;
} while ($after !== null);
return $allSlas;
}The function takes an access token, handles all pagination internally, and returns a clean, flat array. No caller needs to understand that cursor mechanics happened under the hood.
Practitioner Tip "This is the contract a helper function should enforce: simple input, predictable output, zero API complexity visible to callers. The moment calling code has to understand cursor mechanics or inline fragments to consume a helper, the abstraction has failed." |
CLI Output
Raw arrays serve programmatic consumers well. But readable output matters during development, validation, and operational spot-checks.
A simple CLI table turns the result of rkGetSLAs() into something you can scan instantly:
NAME | ID (short) | RETENTION
----------------------------------------------------------------
Gold | 6f101e92… | 365 DAYS
Silver | 1da44b73… | 90 DAYS
Bronze | 0cc22d1b… | 30 DAYS
No-Protection | 3b7a9f45… | N/A
This is immediately useful: a visual SLA inventory, a quick validation tool, and a starting point for piping into scripts or audit exports.
Practitioner Tip "Readable CLI output is underrated. Before building dashboards or integrations, make sure your data is easy to inspect manually. If it is not readable in the terminal, it is probably not structured cleanly enough to be trusted by the automation consuming it." |
What This Week Really Delivers
The extended GraphQL query is not the milestone. It is straightforward.
The milestone is the structure surrounding it. With this week's work, the framework can now:
- Retrieve the complete SLA inventory from any RSC environment regardless of size
- Handle cursor-based pagination safely and transparently
- Normalize API responses into clean, reusable data structures
- Present operational output that is immediately usable
This is the first function in the framework that behaves like a production operational tool rather than an API demonstration. The pattern established here (paginate properly, normalize output, hide API complexity from callers) is the pattern every subsequent function will follow.
Commit of the Week
rkGetSLAs() extended: cursor-based pagination, retention normalization, clean CLI output.
What's Next
With SLA Domains fully queryable, the logical next step is connecting policies to actual protected objects.
In Week 6, we move into snapshot data. Using snappableConnection, RSC's unified query across all protected workload types, we will build rkGetSnapshotCount() to retrieve snapshot statistics for VMware VMs and filesets:
- Total, local, SLA-driven, on-demand, replica, and archive snapshot counts
- A single helper that works across multiple workload types without code changes
- The foundation for compliance reporting, protection gap detection, and backup visibility workflows
This is where the framework transitions from data extraction to operational insight.
Contributed by

Frederic Lhoest
Senior Technology Architect, PCCW Global

Mike Preston
Staff Technical Marketing Manager, Rubrik








