LLM.txt

QLIKCLOUDCLI SCRIPTING CONTRACT
Format: plain text with PowerShell examples. Audience: LLMs and script authors.
Scope: the compiled QlikCloudCLI PowerShell module distributed through the
PowerShell Gallery or as a ZIP.

PURPOSE AND AUTHORITY

Generate executable PowerShell from the installed module's actual command
metadata, matching commands.json, model types, and relevant Qlik API docs.
This file guides use of that metadata; the catalog supplies the complete inventory.
The inventory changes when upstream schemas change. Read commandCount for the
distinct REST command count; do not use operations.Count as the exported-command
count. Use Get-Command -Module QlikCloudCLI for all exported commands, including
constructors and handwritten commands. Inspect the installed catalog rather
than assuming a count or inventory from a previous release.

PUBLIC COMMAND NAMING

REST and request-model command nouns start with QC, for example Get-QCApiKeys
and New-QCCollectionsCollectionsCreateCollectionRequestBodyObject. REST nouns
omit whole path placeholder segments: /collections/{collectionId}/items maps to
QCCollectionsItems. Nested resources remain separate commands. Direct GET
list/detail pairs use one command with List (default) and By<PublicId> parameter
sets, for example Get-QCCollections -CollectionId uses ByCollectionId. A singleton
operation uses Default. Resolve all names and sets from commands.json rather
than inventing a ById command suffix. Constructors still follow
New-QC<Api><Type>Object, including any ById text within the model name itself.
Connection lookup and token exchange are Get-QCConnection and Receive-QCToken.
Connect-QlikCloud and Disconnect-QlikCloud keep their full names. Get-QlikUserAgent
and Invoke-OpenApp keep their names. There are no compatibility aliases for the
previous long REST or constructor names. Do not invent Connect-QC or Disconnect-QC.
The module name is still QlikCloudCLI, and DLL names, paths, error identifiers,
and .NET namespaces/types such as QlikCloud.Common.Connection are unchanged.
Short command names do not imply a QC namespace or a QC module name.

Engine commands use the shared QSE noun prefix, not QC: Open-QSEApp,
Get-QSEScript, Get-QSESheetList and Close-QSEApp, for example. Read the separate
engine-commands.json inventory and Get-Command metadata. QlikSenseCLI builds
with this Engine feature export the same names and parameters. Older installed
versions might not include it; check before generating a portable script.

Preserve API namespaces when selecting commands. The catalog API identity
analytics/apps differs from apps; their command nouns begin QCAnalyticsApps
and QCApps, and their CLR models belong to QlikCloud.AnalyticsApps and
QlikCloud.Apps respectively. Similarly, workflows/automations uses
QCWorkflowsAutomations. Namespaced and legacy APIs can coexist. Never infer
a replacement route, reuse a model from the other API, or drop the namespace.
Match the full catalog api, HTTP method, and path before generating a call.

Explicit route-name exceptions avoid ambiguous parameter unions:
Get-QCAppsReportFiltersById, Get-QCDirectAccessAgentsConnectorsFilesByFileType,
Get-QCSharingTasksExecutionsByExecutionId, and
Invoke-QCDataFilesActionsChangeSpaceById, and Get-QCAnalyticsAppsEvaluationsById.
These exceptions do not establish a
general naming rule. Get-QCAudits uses AuditId for the detail route; its List
set retains Id as the list filter. Never substitute one for the other.

Authority order for a particular installed module:
1. Get-Command: exported names, parameter types, sets, aliases, binding rules.
2. commands.json beside the module manifest: operation-to-command/parameter-set
   mapping, request-model constructors, SDK method bindings, outputs and routes.
3. Matching QlikCloud.operations.json and installed CLR model types: additional
   operation bindings and available request/response properties and enum values.
4. COMMANDS.md: the complete readable command/route index.
5. README.md and this file: packaged usage guidance and examples.
6. Official Qlik API reference: required payload fields, limits, filtering,
   pagination, permissions and other semantics absent from the local metadata.
   Start at [Qlik REST APIs](https://qlik.dev/apis/rest/) and match the exact
   HTTP method and path recorded in commands.json.

Never invent a cmdlet, model, parameter, scope, filter expression, response
property or pagination convention. Do not translate a REST route into a guessed
cmdlet name. Resolve it from the catalog. Do not assume a newer online API
operation exists in the installed module. If required metadata is missing,
consult the official reference for that route or state the missing requirement.
Online examples may target a different API version or tool; they do not create
additional parameters or commands in this module. QlikCloudCLI is a PowerShell
module, not the separate qlik executable: do not substitute qlik CLI syntax.
Get-Help currently provides native cmdlet syntax; there is no generated MAML
manual. Get-Command -Syntax and COMMANDS.md are the reliable local entry points.

ENVIRONMENT AND IMPORT

Target Windows PowerShell 5.1 (Desktop), included with Windows. Run scripts with
powershell.exe. Do not require users to install another PowerShell runtime.
Gallery packages and ZIPs contain the compiled module and its dependencies.
No compilation, development SDK, package restore or source checkout is needed.
For a published Gallery release, users who request installation can run
Install-Module -Name QlikCloudCLI -Repository
PSGallery -Scope CurrentUser, then Import-Module QlikCloudCLI. Do not install
or update modules as an incidental side effect of an ordinary generated script.
For ZIP installation, extract the whole archive and preserve all files and the
bin directory. Import the extracted manifest:

```powershell
# Use caller-supplied ZIP and extraction paths. Prefer a new, empty destination.
$zipPath = 'C:\Downloads\QlikCloudCLI.zip'
$extractDirectory = 'C:\QlikModules'
# Unblock only a downloaded package from a source the caller trusts.
Unblock-File -LiteralPath $zipPath -ErrorAction Stop
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDirectory -ErrorAction Stop
$modulePath = Join-Path $extractDirectory 'QlikCloudCLI/QlikCloudCLI.psd1'
if (-not (Test-Path -LiteralPath $modulePath -PathType Leaf)) { throw 'Module manifest is missing from the extracted package.' }
$module = Import-Module -Name $modulePath -PassThru -ErrorAction Stop
$catalogPath = Join-Path $module.ModuleBase 'commands.json'
$catalog = Get-Content -LiteralPath $catalogPath -Raw | ConvertFrom-Json
Get-Command -Module QlikCloudCLI
```

For later sessions, import that manifest path directly; extraction is a one-time
installation step. Use a fresh powershell.exe process when switching module versions:
Import-Module -Force does not reliably unload previously loaded .NET assemblies.
An existing installation on PSModulePath can be loaded with Import-Module
QlikCloudCLI; when multiple versions exist, inspect the returned module's Path
and use the intended manifest explicitly. Do not import individual DLLs instead
of the manifest or mix dependencies/catalogs from different ZIP versions.

Unblock the trusted ZIP before extraction. If files were already extracted,
scope recovery to that package's QlikCloudCLI directory, never a whole drive
or Downloads directory. HOW-TO.md contains the recovery command. Do not change
or bypass the caller's PowerShell execution policy to import the module.

For a signed release, optional verification checks only the manifest and the
two module DLLs; do not infer that every bundled dependency has the same signer:

```powershell
$signaturePaths = @(
    $modulePath
    (Join-Path $module.ModuleBase 'bin/QlikCloud.dll')
    (Join-Path $module.ModuleBase 'bin/QlikCloudCLI.dll')
)
Get-AuthenticodeSignature -LiteralPath $signaturePaths -ErrorAction Stop |
    Select-Object Path, Status, @{ Name = 'Signer'; Expression = { $_.SignerCertificate.Subject } }
```

For these files, expect Valid and verify the publisher's identity. Trust
evaluation depends on the local machine's certificate configuration. Report a
failed validation for the caller to resolve through their certificate policy;
do not treat an unknown signer or trust failure as proof of a valid release.

All delivered scripts must parse and run in Windows PowerShell 5.1. Use that
runtime for validation, not a newer parser. Read-Host supports -AsSecureString;
it does not support -MaskInput. ConvertFrom-Json supports neither -AsHashtable
nor -Depth in 5.1. ConvertTo-Json -Depth is supported. Avoid ForEach-Object
-Parallel, the ternary operator, null-coalescing/null-conditional operators,
pipeline-chain operators, and encoding names such as utf8NoBOM. Export-Csv
-Encoding UTF8 writes a UTF-8 BOM in this runtime. The ::new constructor syntax
is available in 5.1. Load System.Web with Add-Type -AssemblyName System.Web
before using System.Web.HttpUtility in a fresh session.

For optional current-user installation, derive the WindowsPowerShell\Modules
directory from $PROFILE.CurrentUserAllHosts in powershell.exe; do not use the
PowerShell\Modules directory associated with other PowerShell editions.

CATALOG DISCOVERY RECIPE

The following are local metadata operations; they make no tenant requests.
Catalog formatVersion 2 contains:
- commands: one entry per distinct REST command, with commandName,
  defaultParameterSetName, parameterSetNames and operationCount.
- commandCount: the number of distinct REST commands, excluding constructors
  and handwritten commands.
- operations: one entry per HTTP operation, including commandName,
  parameterSetName and that operation's parameters/required flags/output types.
- constructors: the local request-model constructor inventory.

commandName is not unique in operations. Select a route using its HTTP method
and exact path (and API context where needed), require exactly one match, then
use its parameterSetName to select Get-Command metadata. Do not union mandatory
parameters across different sets. List is the default only for a grouped pair;
the detail identifier selects its By<PublicId> set. List-only filters and
detail-only parameters cannot be mixed. A parent identifier required by both
nested routes does not select the detail set; the additional child identifier
does. Do not infer unrelated routes from a command's combined parameters.

For signed packages, catalog.assemblySha256 describes the delivered, signed
QlikCloud.dll. The optional unsignedAssemblySha256 records the original unsigned
generator-input hash for provenance; do not compare that field to the installed
signed DLL. The delivered file must match assemblySha256.

```powershell
# Check that the catalog belongs to the packaged SDK and operation inventory.
$sdkPath = Join-Path $module.ModuleBase 'bin/QlikCloud.dll'
$inventoryPath = Join-Path $module.ModuleBase 'QlikCloud.operations.json'
if ((Get-FileHash -LiteralPath $sdkPath -Algorithm SHA256).Hash -ine $catalog.assemblySha256) {
    throw 'SDK and command catalog do not match; import a complete matching package.'
}
if ((Get-FileHash -LiteralPath $inventoryPath -Algorithm SHA256).Hash -ine $catalog.clientManifestSha256) {
    throw 'Operation inventory and command catalog do not match.'
}
$catalog.operations | Group-Object api | Sort-Object Name | Select-Object Name, Count
$catalog.commands | Select-Object commandName, defaultParameterSetName, parameterSetNames, operationCount

# Resolve an exact HTTP operation, not a guessed command name.
$operationMatches = @($catalog.operations | Where-Object {
    $_.api -eq 'collections' -and
    $_.path -ceq '/api/v1/collections' -and $_.httpMethod -eq 'POST'
})
if ($operationMatches.Count -ne 1) { throw 'Expected exactly one matching operation.' }
$operation = $operationMatches[0]
$operation | Select-Object commandName, parameterSetName, httpMethod, path, outputType, rawResponseType, supportsShouldProcess
$operation.parameters | Select-Object name, type, mandatory, pipelineByValue, pipelineByPropertyName, aliases
Get-Command -Name $operation.commandName -Module QlikCloudCLI -Syntax
$command = Get-Command -Name $operation.commandName -Module QlikCloudCLI
$parameterSets = @($command.ParameterSets | Where-Object Name -EQ $operation.parameterSetName)
if ($parameterSets.Count -ne 1) { throw 'Catalog parameter set is absent or ambiguous in the installed command.' }
$parameterSets[0].Parameters | Select-Object Name, ParameterType, IsMandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Aliases

# Find the exact constructor for this operation's typed body.
$bodyParameter = $operation.parameters | Where-Object name -eq 'Body'
$constructors = @($catalog.constructors | Where-Object modelType -eq $bodyParameter.type)
$constructors | Select-Object commandName, modelType, supportsJson, collectionElementType
$constructors.parameters | Select-Object name, type, mandatory, constructorOnly
foreach ($constructor in $constructors) {
    Get-Command -Name $constructor.commandName -Module QlikCloudCLI -Syntax
}
```

Operation parameters describe endpoint-specific parameters. The inherited
Connection, RawResponse, Logger, common PowerShell parameters, and conditional
WhatIf/Confirm parameters are discovered with Get-Command. Constructor Json and
InputObject parameter sets also require Get-Command. Catalog type strings use
the C# global:: prefix; that prefix is not part of PowerShell type syntax.
Use modelType to match constructors; do not strip or approximate model names.
Not every object/union request body resolves to one constructor. Inspect the
available constructors and official request-body definition when the declared
body type is System.Object. The ZIP does not include API schema files. Do not
request local schema paths or a development environment to author scripts.

Inspect compiled model properties locally without making a request. The
JsonProperty metadata identifies JSON field names and serialization constraints;
it does not contain every server-side requirement, validation rule or default.

```powershell
$bodyType = (Get-Command $operation.commandName -Module QlikCloudCLI).Parameters['Body'].ParameterType
foreach ($property in $bodyType.GetProperties()) {
    $json = $property.GetCustomAttributes([Newtonsoft.Json.JsonPropertyAttribute], $true) | Select-Object -First 1
    $isExtensionData = $property.IsDefined([Newtonsoft.Json.JsonExtensionDataAttribute], $true)
    [pscustomobject]@{
        Property = $property.Name
        Type = $property.PropertyType.FullName
        JsonName = if ($isExtensionData) { '(extension data)' } elseif ($null -ne $json) { $json.PropertyName } else { $property.Name }
        JsonRequired = if ($null -ne $json) { [string]$json.Required } else { '' }
        ExtensionData = $isExtensionData
    }
}
# Inspect the selected operation's response model without calling the tenant.
[QlikCloud.Collections.CollectionResultResponseBody].GetProperties() |
    Select-Object Name, PropertyType
```

For an enum parameter, discover CLR names instead of guessing capitalization,
punctuation or wire-value conversion. Json constructors accept API wire
values through the SDK JSON converters. CLR enum members can carry an
EnumMemberAttribute whose Value differs from their C# name.

```powershell
$parameter = (Get-Command Get-QCItems -Module QlikCloudCLI).Parameters['ResourceType']
$enumType = [Nullable]::GetUnderlyingType($parameter.ParameterType)
if ($null -eq $enumType) { $enumType = $parameter.ParameterType }
[Enum]::GetNames($enumType)
```

For payload semantics, use the official reference for the exact route. For
example, [Collections REST](https://qlik.dev/apis/rest/collections/) documents
the create body and [Users REST](https://qlik.dev/apis/rest/users/) documents
user filters and page limits. Map those JSON fields to the installed constructor
and its model metadata. Do not assume a property has a value in every response:
optional fields such as user Subject may be omitted. If online docs and the
installed type differ, explain the compatibility gap rather than inventing a
property or silently substituting a different operation.
An AdditionalProperties dictionary marked as extension data contributes its
entries as JSON fields; it is not a literal additionalProperties request field.

CONNECTIONS, AUTHENTICATION AND SECRETS

Use a generic tenant hostname or base HTTPS URL, without /api, a query, fragment
or embedded credentials. Obtain $apiKey, $clientId, $clientSecret and $scope
from the caller's chosen configuration/secret provider. Do not assume a secret
provider module is installed; do not embed credentials in source or examples.
Never print tokens, connection internals, authorization headers or full token
responses. Do not serialize a connection into logs, CSV, JSON or a transcript.

For an interactive API-key prompt in Windows PowerShell 5.1:

```powershell
$secureApiKey = Read-Host 'API key' -AsSecureString
try {
    $apiKey = ([pscredential]::new('QlikCloudAPIKey', $secureApiKey)).GetNetworkCredential().Password
    $connection = Connect-QlikCloud -Tenant $tenant -APIKey $apiKey -PassThru -ErrorAction Stop
}
finally {
    $apiKey = $null
    $secureApiKey.Dispose()
    $secureApiKey = $null
}
```

The connection remains open for subsequent commands and retains its credential
until disposed. Disconnect it in the enclosing workflow's finally block. The
examples below are alternatives to this prompt, not additional connections to
create without disposing earlier ones.

```powershell
$tenant = 'tenant.region.qlikcloud.com'

# Explicit connection: leaves the current runspace default unchanged.
$connection = Connect-QlikCloud -Tenant $tenant -APIKey $apiKey -PassThru -ErrorAction Stop
try {
    $page = Get-QCUsers -Limit 20 -Connection $connection -ErrorAction Stop
    $page.Data | Select-Object Id, Name, Email
}
finally {
    Disconnect-QlikCloud -Connection $connection -Confirm:$false
}

# Alternative: establish a default in the current runspace; no output by default.
Connect-QlikCloud -Tenant $tenant -APIKey $apiKey
$current = Get-QCConnection
Disconnect-QlikCloud

# Alternative OAuth flows; use the scopes/client registration actually provisioned.
Connect-QlikCloud -Tenant $tenant -ClientID $clientId -Scope $scope
Connect-QlikCloud -Tenant $tenant -ClientID $clientId -ClientSecret $clientSecret -ClientCredentials -Scope $scope
```

The alternatives above are independent examples, not a sequence of required
authentication steps. API-key Connect constructs a client without validating
tenant access; a successful authorized request establishes actual access.
Client-credentials OAuth requires both ClientID and ClientSecret. Requested
scopes must be allowed by the OAuth client registration; effective permissions
also depend on the calling identity and resource access. Do not invent a scope
that supposedly grants all operations. Consult the operation and current Qlik
scope guidance: https://qlik.dev/manage/access-control/scopes/ and
https://qlik.dev/apis/rest/oauth/ .

The default connection is per PowerShell runspace, not process-global. Explicit
-Connection takes precedence. Child/parallel runspaces do not inherit a default;
import the module and manage a connection in each worker. Prefer sequential
iteration unless concurrency is requested and connection/rate-limit lifetimes
are designed. Disconnect closes the selected HTTP transport and clears the
default only if it is that same connection. A disposed connection cannot be
reused. Do not dispose a connection while another operation is using it.

Receive-QCToken accepts -Connection and requires OAuth; TokenPurpose
defaults to Websocket. It returns a sensitive IdentityModel TokenResponse, not
a plain token string. Assign it to a variable only when the task needs token
exchange, and pass its AccessToken directly to the authorized consumer without
echoing it. JWT authentication via -JWTToken is explicitly unsupported and
fails with QlikCloudJwtNotSupported; do not treat it as an OAuth alternative.
Get-QlikUserAgent returns only the user-agent string.

REQUEST MODELS, NULLABILITY AND PIPELINES

New-*Object commands in catalog.constructors only construct local CLR models.
They perform no HTTP request. New-* REST commands in catalog.operations can
create remote resources. Distinguish them using the catalog, not just the verb.

Normal model constructors accept named properties, -Json, or typed -InputObject.
Json and InputObject are mutually exclusive. Named properties can override
either input when their parameter set permits it. InputObject is copied through
SDK JSON serialization before overrides; the source and nested model properties
are not mutated. Only explicitly bound properties are overridden. Do not filter
splat values using truthiness: $false, 0 and empty strings are deliberate values.
Omit an optional endpoint parameter when it should be absent. Optional bools
such as -TotalResults and -NoData are bool values, not SwitchParameter flags;
write -TotalResults $false or -NoData $true. RawResponse and WhatIf are switches.

Required endpoint/body parameters are enforced by PowerShell binding. Many
constructor properties are optional to the binder even when the API
requires them. Supply required API fields and obey endpoint-specific bounds;
a successfully constructed object does not prove the server will accept it.
Do not infer an API default from the CLR default of an uninitialized property.

```powershell
# JSON wire value "private" maps through the generated enum converter.
$bodyJson = @{ name = 'Example collection'; type = 'private'; description = 'Example only' } |
    ConvertTo-Json -Depth 10 -Compress
$body = New-QCCollectionsCollectionsCreateCollectionRequestBodyObject -Json $bodyJson
New-QCCollections -Body $body -WhatIf

# Local conversion example only: source stays true/5, copy contains false/0.
$original = New-QCReloadsReloadRequestObject -AppId $appId -Partial $true -Weight 5
$copy = New-QCReloadsReloadRequestObject -InputObject $original -Partial $false -Weight 0
```

Collection-model constructors accept -Items; supplying Items replaces the
collection contents. Json may be an array when the request model is a collection,
for example New-QCCollectionsCollectionByIdPatchObject. Constructors
preserve the collection as a single typed body object. FileParameter constructors
are different: no Json support, and InputObject copies the wrapper while sharing
the same open stream. Do not combine their constructor-only properties with
InputObject; Get-Command exposes the exact parameter sets.

Path identifiers bind by property name. A single path identifier generally also
binds by value; verify aliases and rules in the selected operation and parameter
set. An Id alias is absent when it conflicts with a real parameter, such as a
list filter; use the public path identifier instead. Multiple
path identifiers require explicitly named arguments or matching properties.
Body binds typed objects by value. Each pipeline record invokes one SDK call;
there is no implicit bulk request or automatic pagination. Prefer explicitly
named IDs when the input object's Id could refer to a different resource.

```powershell
# Known IDs supplied by the caller; one GET per input string.
$userIds | Get-QCUsers -Connection $connection
# Prefer this for bodies when the path ID is fixed.
$patchBody | Update-QCCollections -CollectionId $createdId -Connection $connection -WhatIf
```

RESPONSES AND ERRORS

Default REST output unwraps APIResponse<T>.Object and emits the typed payload.
An API collection envelope remains an envelope; explicitly select its Data or
other documented collection property. A payload that is itself enumerable
can be enumerated by the PowerShell pipeline. Use @(...) when caller code needs
stable zero/one/many array semantics. Do not assume all endpoints return Data.

-RawResponse returns the SDK response wrapper as one object, retaining Object,
StatusCode, Headers and RawContent when that response type supports them.
An endpoint without a typed response body returns APIResponse directly, with no
Object property. FileResponse is already a streaming response and is not wrapped
in another Object property. Inspect outputType/rawResponseType and actual CLR
metadata before accessing properties. RawContent may contain sensitive records;
select only the fields needed for the task instead of printing a full wrapper.

```powershell
$response = Get-QCUsers -Limit 20 -Connection $connection -RawResponse -ErrorAction Stop
$status = $response.StatusCode
$users = @($response.Object.Data)
```

HTTP/transport failures are terminating errors. Use try/catch and -ErrorAction
Stop for clear script boundaries. HTTP errors use QlikCloudHttpError with a
QlikCloudCLI.Runtime.QlikCloudApiException whose StatusCode is available.
Categories include 400 InvalidArgument, 401 AuthenticationError, 403
PermissionDenied, 404 ObjectNotFound and 429 LimitsExceeded. A missing or disposed
connection reports QlikCloudConnectionRequired. Invalid constructor JSON reports
QlikCloudInvalidModelJson. Generic operation failures use QlikCloudOperationFailed.
Do not require an exact FullyQualifiedErrorId string: PowerShell may append the
implementing cmdlet type. Do not dump raw request/server bodies or credentials
when adding error handling; log an operation label, status and category instead.

```powershell
try {
    $result = Get-QCCollections -CollectionId $collectionId -Connection $connection -ErrorAction Stop
}
catch {
    $statusProperty = $_.Exception.PSObject.Properties['StatusCode']
    if ($null -ne $statusProperty) {
        Write-Warning ('Collection read failed with HTTP {0}.' -f $statusProperty.Value)
    }
    throw
}
```

REST calls pass cancellation to SDK asynchronous methods. Internal
SDK retries can mean more than one HTTP attempt; do not promise exactly-once
effects or automatically replay a non-idempotent create, reload, import or stream
upload. Bound retries and respect endpoint/rate-limit guidance when adding them.

PAGINATION: USERS EXAMPLE, NOT A UNIVERSAL ALGORITHM

There is no universal -All, -Page, -PageSize, offset or cursor convention.
Get-QCUsers accepts -Limit (documented range 1..100), -Next and -Prev. Its
response uses Data and Links.Next.Href. Next is a cursor token, not a whole URL;
do not pass Href directly as -Next. Some responses use the legacy startingAfter
query key in that link: extract its value and pass it as -Next.
The module has no -StartingAfter parameter on Get-QCUsers. The helper
below accepts next first, falling back to startingAfter. Do not combine Next and
Prev. Preserve
filters/sort/limit across pages. Stop when no next link exists, not merely when
a page contains fewer records than the requested limit. Other APIs need their
own documented pagination logic, even when their parameter names look similar.

This is a user-defined helper, not an exported module command. It performs
read-only tenant requests when invoked. A page cap and repeated-cursor check
turn incomplete enumeration into an explicit error instead of silent truncation.

```powershell
function Read-AllQlikCloudUsers {
    param(
        [Parameter(Mandatory)] [QlikCloud.Common.Connection] $Connection,
        [Parameter(Mandatory)] [uri] $TenantBaseUri,
        [ValidateRange(1, 100000)] [int] $MaxPages = 1000
    )
    Add-Type -AssemblyName System.Web
    $query = @{ Connection = $Connection; Limit = 100; ErrorAction = 'Stop' }
    $seen = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
    for ($pageNumber = 1; $pageNumber -le $MaxPages; $pageNumber++) {
        $page = Get-QCUsers @query
        foreach ($user in $page.Data) { $user }
        if ($null -eq $page.Links -or $null -eq $page.Links.Next -or
            [string]::IsNullOrWhiteSpace($page.Links.Next.Href)) { return }
        $nextUri = [uri]::new($TenantBaseUri, $page.Links.Next.Href)
        if ($nextUri.Scheme -ne $TenantBaseUri.Scheme -or
            $nextUri.Authority -ne $TenantBaseUri.Authority -or
            $nextUri.AbsolutePath -cne '/api/v1/users' -or
            -not [string]::IsNullOrEmpty($nextUri.UserInfo)) {
            throw 'Unexpected users pagination link.'
        }
        $linkQuery = [System.Web.HttpUtility]::ParseQueryString($nextUri.Query)
        $cursor = $linkQuery['next']
        if ([string]::IsNullOrEmpty($cursor)) { $cursor = $linkQuery['startingAfter'] }
        if ([string]::IsNullOrEmpty($cursor) -or -not $seen.Add($cursor)) {
            throw 'Missing or repeated users pagination cursor.'
        }
        $query['Next'] = $cursor
    }
    throw 'Users pagination exceeded MaxPages; results are incomplete.'
}

# Gather successfully before writing a final CSV; no Format-Table before Export-Csv.
$allUsers = @(Read-AllQlikCloudUsers -Connection $connection -TenantBaseUri "https://$tenant/")
$allUsers | Select-Object Id, Name, Email | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Encoding utf8
```

Only export the fields and destination the caller requested. The helper's output
can be streamed by callers, but a later error means that stream is incomplete.
For very large inventories, write a temporary file and publish it only after
successful completion instead of holding the entire inventory in memory.
User filters use the API's documented filter syntax; they are not PowerShell
Where-Object expressions. Consult the Users REST reference for allowed
fields/operators.

COMMON WORKFLOW MAP

Task Exact command(s) and important shape
Users inventory Get-QCUsers -> Users.Data, Links.Next.Href
Read a user Get-QCUsers -UserId -> Users.Response;
                              inspect that model, not an assumed plain User
Spaces inventory Get-QCSpaces -> Spaces envelope; own pagination
Catalog/app inventory Get-QCItems -ResourceType App -> items envelope
Read an app Get-QCApps -AppId -> NxApp
Private collections Get-QCCollections (List) or -CollectionId (detail);
                              New/Set/Update/Remove-QCCollections
Collection items Get-QCCollectionsItems -CollectionId (List),
                              add -ItemId for one item
Start a reload New-QCReloads -Body (ReloadRequest)
Read a reload Get-QCReloads -ReloadId
Initiate an app export Invoke-QCAppsExport -AppId -NoData $true
Download temporary content Get-QCTempContents -Id -> FileResponse
Download app media Get-QCAppsMediaFiles -AppId -Path
Upload/replace app media Set-QCAppsMediaFiles -AppId -Path -Body
Open an Engine app Open-QSEApp -AppId -Connection [-NoData]

Get-QCApps requires AppId; it has no list operation in this module. Use
Get-QCItems -ResourceType App for an app catalog page. An app catalog
item's Id is its item ID; ResourceId is the referenced app ID. Do not feed item
IDs to app operations. Discover every workflow's full parameters in the catalog.
Reload submission is not reload completion. If polling is required, inspect the
installed Reload model and official API reference for actual status values,
use a time/page budget, and handle
documented terminal failures; do not invent success states or poll forever.

```powershell
$itemsPage = Get-QCItems -ResourceType App -Limit 20 -Connection $connection
$itemsPage.Data | Select-Object Id, Name, ResourceId
# This iterates this page only; apply the items API's pagination for a full list.
foreach ($item in $itemsPage.Data) {
    $app = Get-QCApps -AppId $item.ResourceId -Connection $connection
    # Select only properties confirmed on NxApp for the requested report.
}
```

MUTATIONS AND A GUARDED COLLECTION LIFECYCLE

supportsShouldProcess in the catalog identifies commands with WhatIf/Confirm.
Mutation WhatIf executes before connection validation or the HTTP call, so it
can be used offline. It still requires valid PowerShell parameter binding and
local model construction. It does not validate remote permissions, complete
payload validity, server availability or the eventual outcome. Do not append
WhatIf to GET commands that do not support it. Constructors have no WhatIf:
constructing their local object does not require one.

Keep preview and execution distinct. Never broaden a requested change or delete
resources by a name/filter search as cleanup. For a test, delete only the exact
resource ID returned by that test's successful create, after verifying that
identity, its unique marker, and an ItemCount of zero. If create succeeds remotely but no ID is
received, report the uncertainty instead of searching and deleting candidates.

The following example defaults to preview. Set execute to true only for an
authorized create/update/delete test. It changes only the collection it creates.

```powershell
$execute = $false
$name = 'cli-example-' + [guid]::NewGuid().ToString('N')
$bodyJson = @{ name = $name; type = 'private'; description = 'Temporary CLI example' } |
    ConvertTo-Json -Depth 10 -Compress
$body = New-QCCollectionsCollectionsCreateCollectionRequestBodyObject -Json $bodyJson
New-QCCollections -Body $body -WhatIf

if ($execute) {
    $createdId = $null
    try {
        $created = New-QCCollections -Body $body -Connection $connection -ErrorAction Stop
        $createdId = [string]$created.Id
        if ([string]::IsNullOrWhiteSpace($createdId)) { throw 'Create returned no ID; do not search-delete.' }
        $current = Get-QCCollections -CollectionId $createdId -Connection $connection
        if ($current.Id -cne $createdId -or $current.Name -cne $name -or
            $current.Type -ne 'Private' -or $current.ItemCount -ne 0) {
            throw 'Created-resource identity check failed.'
        }
        $replacement = New-QCCollectionsCollectionsUpdateCollectionRequestBodyObject -Name $name -Description 'Updated by example'
        $null = Set-QCCollections -CollectionId $createdId -Body $replacement -Connection $connection
        $patch = New-QCCollectionsCollectionByIdPatchObject -Json '[{"op":"replace","path":"/description","value":"Patched by example"}]'
        $null = Update-QCCollections -CollectionId $createdId -Body $patch -Connection $connection
        Get-QCCollections -CollectionId $createdId -Connection $connection |
            Select-Object Id, Name, Description
    }
    finally {
        if (-not [string]::IsNullOrWhiteSpace($createdId)) {
            $owned = Get-QCCollections -CollectionId $createdId -Connection $connection -ErrorAction Stop
            if ($owned.Id -cne $createdId -or $owned.Name -cne $name -or
                $owned.Type -ne 'Private' -or $owned.ItemCount -ne 0) {
                throw 'Cleanup identity check failed; collection was not deleted.'
            }
            $null = Remove-QCCollections -CollectionId $createdId -Connection $connection -Confirm:$false -ErrorAction Stop
        }
    }
}
```

Cleanup failure must be reported with the created ID retained for an operator;
do not hide it or fall back to broad deletion. A workflow requiring durable
recovery should persist its created ID and marker to the caller's approved state
file before later steps. If wrapping these operations in an advanced function,
declare SupportsShouldProcess and honor the caller's WhatIf/Confirm intent.

BINARY DOWNLOADS, EXPORTS AND UPLOADS

FileResponse owns an open Stream and must be disposed. Copy binary bytes to a
file stream; do not use Out-File, Set-Content, text encoding or JSON conversion.
Use an explicit output path and overwrite policy. This example refuses overwrite.
A copy failure can leave a partial file; report it or use a temporary-file and
rename workflow when the caller needs an atomic final output.

```powershell
# $tempContentId and $outputPath are caller-supplied values.
$download = $null
$destination = $null
try {
    $download = Get-QCTempContents -Id $tempContentId -Connection $connection -ErrorAction Stop
    $destination = [System.IO.File]::Open($outputPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
    $download.Stream.CopyTo($destination)
}
finally {
    if ($null -ne $destination) { $destination.Dispose() }
    if ($null -ne $download) { $download.Dispose() }
}
```

Invoke-QCAppsExport is the POST export-initiation call.
It returns APIResponse with a Location header (201), not a FileResponse and not
an app file. There is no -File, -OutFile or -Path convenience parameter on it.
Use -WhatIf for preview. On authorized execution, inspect the Location header,
validate the URI against the expected tenant and /api/v1/temp-contents/{id}
route, and obtain the temp-content ID before calling the download command above.
Do not blindly follow an arbitrary URL with credentials. If its route/query does
not match the known download contract, consult the API reference and stop rather than
discarding query values or guessing a different endpoint. NoData is an explicit
bool: -NoData $true exports object contents without app data.

FileParameter constructor type must match its API group: Apps.FileParameter and
TempContents.FileParameter are different CLR types. Open a new stream for each
actual upload. HTTP content consumes/disposes the supplied upload stream; do not
reuse it for retries or a second request. Finally also disposes streams for
WhatIf and early failures, when HTTP never acquired them.

```powershell
# Preview a media replacement; remove WhatIf only for an authorized target.
$source = [System.IO.File]::OpenRead($inputPath)
try {
    $upload = New-QCAppsFileParameterObject -Data $source -FileName ([System.IO.Path]::GetFileName($inputPath)) -ContentType 'application/octet-stream'
    Set-QCAppsMediaFiles -AppId $appId -Path $mediaPath -Body $upload -Connection $connection -WhatIf
}
finally { $source.Dispose() }
```

ENGINE AND COMPATIBILITY LIMITS

The shared initial Engine surface is exactly these 16 commands:
Open-QSEApp, Close-QSEApp, Get-QSEAppLayout, Get-QSEScript, Test-QSEScript,
Get-QSEField, Get-QSETable, Test-QSEExpression, Invoke-QSEExpression,
Get-QSESheetList, Get-QSEBookmarkList, Get-QSEDimensionList, Get-QSEMeasureList,
Get-QSEMasterObjectList, Get-QSEFieldList, Get-QSEVariableList.
Do not invent Set-QSEScript, Invoke-QSEReload or other future wrapper commands.

Only connection setup/authentication and target app configuration differ between
the two platforms. Use Connect-QlikCloud ... -PassThru or Connect-QlikSense ...
-PassThru, then Open-QSEApp -Connection $connection -AppId $appId. QlikClient is
an alias for Connection on Open-QSEApp. Without that parameter, Open resolves
the selected module's default connection. PassThru does not set that default.
Open returns a live native Qlik.Engine.IApp with callable SDK methods, not a
REST app record or a wrapper requiring .NativeApp. Each open has isolated state.
NoData is a switch on Open-QSEApp; its default is false (load data).

All subsequent commands require -App (also pipeline input), and retain its
original session/connection even if the default changes. Get-QSEScript returns
one string. Test-QSEScript checks the existing app script and never replaces it.
Get-QSEField accepts optional Name and State (default '$'); without Name it
lists fields. Invoke-QSEExpression returns Text, Number and IsNumeric. List
commands emit detached snapshots, not live content handles. -AsSdkObject
returns a native SDK list; dispose it after use. Its app also owns its cleanup.

Use this pattern after connecting to either platform:
```powershell
$app = $null
try {
    $app = Open-QSEApp -Connection $connection -AppId $appId
    $script = Get-QSEScript -App $app
    $nativeScript = $app.GetScript()
    Get-QSESheetList -App $app | Select-Object Id, Title
    Invoke-QSEExpression -App $app -Expression '1 + 1'
}
finally {
    if ($null -ne $app) { Close-QSEApp -App $app }
}
```

Close-QSEApp is idempotent and disposes app/session/retained list resources.
Disconnecting the owning connection closes its remaining apps too. Closing is
not rollback. SDK direct calls can mutate content, bypass WhatIf and wrapper
serialization, and must be sequenced by the caller. Static SDK list helpers are
C# extension methods: use [Qlik.Sense.Client.AppExtensions]::GetSheetList($app),
not an invented $app.GetSheetList() instance method. Prefer the list cmdlets.

The same Engine names collide if both modules are imported. Use one selected
module or qualify ALL Engine commands with their owner (for example
QlikCloudCLI\Get-QSEScript). Do not pass a handle between modules, processes or
runspaces, or use an SDK-created external handle with these wrappers.

Engine cmdlets accept TimeoutSeconds (default 120). Timeout/stop bounds the local
wait; an interrupted operation may invalidate/close its app session. Reopen it
before more operations. Do not claim server cancellation from a local stop.
Legacy Invoke-OpenApp remains available; NoData and LogWS there are Booleans.
Import and REST success do not prove live Engine authorization or compatibility.
Only describe auth/platform workflows as live-tested when actually exercised.

SCRIPT-GENERATION ACCEPTANCE CHECKLIST

Before returning or executing a generated script:
1. Identify the imported package and match its catalog/inventory hashes. Use a
   fresh PowerShell process if another SDK assembly was already loaded.
2. Resolve every endpoint by exact method+route and each body constructor by
   modelType. Check every command and named parameter with Get-Command. Inspect
   parameter sets, mandatory fields, nullable booleans, aliases and enum values.
3. Use compiled model metadata and the matching official API reference for
   payload requirements, response envelope, pagination links/cursors, limits,
   filtering and permissions. Do not assume one API group's rules apply to
   another or require local API schema files.
4. Use explicit connection lifetimes; keep secrets and token responses out of
   output. Do not rely on a default crossing runspaces or create fake JWT support.
5. Parse the script in Windows PowerShell 5.1 and construct its request models offline. Preview
   intended mutations with WhatIf. Local conversion and WhatIf are not proof of
   live authorization or server-side acceptance.
6. Perform authorized read-only checks first. Execute changes only within the
   caller's requested scope. Keep a created-resource ID for recovery; cleanup
   must never target an existing resource merely because its name matches.
7. Handle zero/one/many results and all required pages. Bound polling/retries.
   Preserve false/zero values. Never convert binary streams to text.
8. Dispose file responses, file streams, connections and Engine sessions through
   their appropriate lifecycles. Report partial results or cleanup failure.
9. State what was actually validated: metadata/parse, offline behavior, WhatIf,
   or specific live requests. Do not imply tenant/Engine access from import or
   from an offline test alone.

Local parse example (does not execute the script):

```powershell
$tokens = $null
$parseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors.Message -join [Environment]::NewLine) }
```

When delivering a script, include its required PowerShell version, required input
variables, how to import the matching module, which calls read or mutate remote
state, expected output shape, and its actual validation boundary. Prefer a
small exact script over a broad guessed abstraction that hides tenant effects.