Skip to content

Level.io port: 43 scripts cannot receive their runtime inputs (84 variables) #47

Description

@limehawk

Summary

43 scripts (84 runtime variables) will run on Level.io but cannot receive their inputs. They fail safe — the guard if ([string]::IsNullOrWhiteSpace($X) -or $X -eq '$x') catches the unsubstituted placeholder and exits with an error — but they do nothing useful until ported.

This issue documents the mapping and the open questions. No scripts have been changed.

Why they break

SuperOps text-substitutes $snake_case runtime variables into the script before execution:

$ProfileToKeep = "$profile_to_keep"     # SuperOps replaces this

Level does not. On Level, $profile_to_keep is an undefined PowerShell variable and expands to an empty string.

How Level actually passes input

Level renders {{...}} into the script body at run time. Verbatim from System Variables:

To reference a variable in a script directly, use the {{variable_name}} syntax:
echo "Running on {{level_device_hostname}} in {{level_group_path}}"

Three sources, and only three:

  1. System variableslevel_*, always available (hostname, group path, IPs, tags…). No setup.
  2. Custom fields — referenced by a derived token. Level's OpenAPI spec: custom_field.reference, "How the field is referenced in automation scripts", example cf_region. Values settable global → group → device.
  3. Automation variables — set by an earlier action in an automation pipeline.

There is no per-run input prompt. Nothing asks you for a value when you click Run. This is the core behavioural difference from SuperOps.

The Variables tab on a script is output onlyAutomation Variables: "Script variables are for capturing output, not supplying input."

Verified facts

  • createCustomField(input:{name!, adminOnly, value, description}). reference is derived, not settable: creating a field named ZZ Test Profile To Keep yielded reference: cf_zz_test_profile_to_keep. So the token is cf_ + snake_case(name). (Created and deleted during testing; the tenant currently has 0 custom fields.)
  • Not verified: I have not executed a script on a device to watch {{cf_x}} substitute. Syntax is documented and reference is spec'd, but end-to-end substitution is inference until it runs once. Pilot one script before porting 43.

The port

# before (SuperOps)
$ProfileToKeep = "$profile_to_keep"

# after (Level) — custom field named "Profile To Keep"
$ProfileToKeep = "{{cf_profile_to_keep}}"

The existing empty/placeholder guard still works and should be kept.

⚠️ Secrets: Level has nowhere good to put them

18 of the 84 variables are secrets (tokens, passwords, API keys). I checked:

  • No secrets store. No secrets/vault/credentials resource in Level's REST v2 spec or its GraphQL schema.
  • Custom fields are plain values. The CustomField type exposes id, name, reference, adminOnly, value — no secret, masked, encrypted, or sensitive field, and no field-type enum. The docs never mention masking.
  • admin_only is access control, not protection: "This field's value can only be viewed or edited by an account administrator."
  • The value is rendered into the script body before execution, so the command text delivered to the agent contains the secret.

That is materially weaker than a SuperOps runtime value typed at launch and never stored. Putting CloudflaredTunnelToken, NewPassword or ACCOUNT_KEY into a custom field is a real trust decision, not a mechanical port. Recommend: do not port the 18 secret-taking variables until this is decided.

Blast radius note

delprof2_* deletes user profiles. If a custom field is unset or wrong, that matters. The current guard fails safe; keep it, and pilot on a single device.

Mapping

script SuperOps var proposed Level custom field secret?
cloudflared_install CloudflaredTunnelToken {{cf_cloudflared_tunnel_token}} yes
delprof2_old_profiles_delete days_old {{cf_days_old}}
delprof2_selected_profile_keep profile_to_keep {{cf_profile_to_keep}}
delprof2_specific_profile_delete profile_to_delete {{cf_profile_to_delete}}
directory_cleanup runtime_folderPath {{cf_runtime_folder_path}}
directory_cleanup runtime_days {{cf_runtime_days}}
directory_size_analyze YourTargetDirectoryHere {{cf_your_target_directory_here}}
dns_set_provider DnsProviderSelection {{cf_dns_provider_selection}}
files_rename_snake_case YourTargetPathHere {{cf_your_target_path_here}}
gcpw_install YourDomainsHere {{cf_your_domains_here}}
gcpw_install YourEnrollmentTokenHere {{cf_your_enrollment_token_here}} yes
huntress_install_macos ACCOUNT_KEY {{cf_a_c_c_o_u_n_t_k_e_y}} yes
huntress_install_macos ORG_KEY {{cf_o_r_g_k_e_y}} yes
huntress_install_macos INSTALL_SYSTEM_EXTENSION {{cf_i_n_s_t_a_l_l_s_y_s_t_e_m_e_x_t_e_n_s_i_o_n}}
local_user_admin_create NewAdminUsername {{cf_new_admin_username}}
local_user_admin_setup NewAdminUsername {{cf_new_admin_username}}
local_user_admin_setup NewAdminFullName {{cf_new_admin_full_name}}
local_user_admin_toggle TargetUsername {{cf_target_username}}
local_user_create NewUsername {{cf_new_username}}
local_user_create NewPassword {{cf_new_password}} yes
local_user_delete UsernameToDelete {{cf_username_to_delete}}
local_user_disable UsernameToDisable {{cf_username_to_disable}}
maintenance_reboot graceful_true_or_false {{cf_graceful_true_or_false}}
maintenance_reboot maxuptimedays {{cf_maxuptimedays}}
mrt_scan QuickOrFull {{cf_quick_or_full}}
msi_url_install MSIURL {{cf_m_s_i_u_r_l}}
nirsoft_uninstall_view YourAppPatternHere {{cf_your_app_pattern_here}}
password_files_search GoogleChatWebhook {{cf_google_chat_webhook}}
password_files_tickets_search SuperOpsApiKey {{cf_super_ops_api_key}} yes
password_files_tickets_search GoogleChatWebhook {{cf_google_chat_webhook}}
prinstall_add YourPrinterIpHere {{cf_your_printer_ip_here}}
prinstall_add YourUsbQueueNameHere {{cf_your_usb_queue_name_here}}
prinstall_add YourDriverNameHere {{cf_your_driver_name_here}}
prinstall_add YourPrinterNameHere {{cf_your_printer_name_here}}
prinstall_driver_add YourDriverTargetHere {{cf_your_driver_target_here}}
prinstall_driver_add YourDriverNameHere {{cf_your_driver_name_here}}
prinstall_driver_remove YourDriverTargetHere {{cf_your_driver_target_here}}
prinstall_drivers YourIpHere {{cf_your_ip_here}}
prinstall_drivers YourModelHere {{cf_your_model_here}}
prinstall_id YourIpHere {{cf_your_ip_here}}
prinstall_remove YourPrinterTargetHere {{cf_your_printer_target_here}}
prinstall_scan YourSubnetHere {{cf_your_subnet_here}}
prinstall_scan ScanModeAllNetworkUsb {{cf_scan_mode_all_network_usb}}
prinstall_setup InstallOrUninstall {{cf_install_or_uninstall}}
prinstall_trust_codesign CertPath {{cf_cert_path}}
restic_b2_backup_install B2KeyId {{cf_b2_key_id}} yes
restic_b2_backup_install B2AppKey {{cf_b2_app_key}} yes
restic_b2_backup_install B2BucketName {{cf_b2_bucket_name}}
restic_b2_backup_install RepoPassword {{cf_repo_password}} yes
restic_b2_backup_install ClientName {{cf_client_name}}
rustic_install YourBackendType {{cf_your_backend_type}}
rustic_install YourBackendPath {{cf_your_backend_path}}
rustic_install YourRepoPassword {{cf_your_repo_password}} yes
rustic_install YourClientName {{cf_your_client_name}}
rustic_install YourBackendKeyId {{cf_your_backend_key_id}} yes
rustic_install YourBackendAppKey {{cf_your_backend_app_key}} yes
rustic_install YourBackendRegion {{cf_your_backend_region}}
rustic_install YourBackupPaths {{cf_your_backup_paths}}
rustic_install YourExcludePatterns {{cf_your_exclude_patterns}}
rustic_install YourBackupHour {{cf_your_backup_hour}}
rustic_install_unix YourBackendType {{cf_your_backend_type}}
rustic_install_unix YourBackendPath {{cf_your_backend_path}}
rustic_install_unix YourRepoPassword {{cf_your_repo_password}} yes
rustic_install_unix YourClientName {{cf_your_client_name}}
rustic_install_unix YourBackendKeyId {{cf_your_backend_key_id}} yes
rustic_install_unix YourBackendAppKey {{cf_your_backend_app_key}} yes
rustic_install_unix YourBackendRegion {{cf_your_backend_region}}
rustic_install_unix YourBackupPaths {{cf_your_backup_paths}}
rustic_install_unix YourExcludePatterns {{cf_your_exclude_patterns}}
rustic_install_unix YourBackupHour {{cf_your_backup_hour}}
rustic_restore YourSnapshotId {{cf_your_snapshot_id}}
rustic_restore YourRestorePath {{cf_your_restore_path}}
rustic_restore YourDestination {{cf_your_destination}}
rustic_restore_unix YourSnapshotId {{cf_your_snapshot_id}}
rustic_restore_unix YourRestorePath {{cf_your_restore_path}}
rustic_restore_unix YourDestination {{cf_your_destination}}
sentinelone_install SiteToken {{cf_site_token}} yes
superops_service_restart YourServiceFilterHere {{cf_your_service_filter_here}}
terminated_user_lockout TerminatedUsername {{cf_terminated_username}}
winget_package_install PackageName {{cf_package_name}}
workstation_rename_auto YourApiKeyHere {{cf_your_api_key_here}} yes
workstation_rename_manual YourApiKeyHere {{cf_your_api_key_here}} yes
workstation_rename_manual YourCustomClientHere {{cf_your_custom_client_here}}
workstation_rename_manual_macos YourCustomClientHere {{cf_your_custom_client_here}}

Asset-field placeholders (no custom field; use system variables)

  • workstation_rename_auto: YourAssetIdHere, YourAssetNameHere, YourClientNameHere
  • workstation_rename_auto_macos: YourClientNameHere
  • workstation_rename_manual: YourAssetIdHere, YourAssetNameHere, YourClientNameHere
  • workstation_rename_manual_macos: YourClientNameHere

Asset-field placeholders → system variables

These 4 scripts use SuperOps asset-field placeholders, which have no custom-field equivalent. Map them to Level system variables instead (level_device_id, level_device_hostname, level_group_name/level_group_path), not to custom fields.

Suggested next step

Pilot exactly one non-secret script (delprof2_selected_profile_keep), confirm {{cf_profile_to_keep}} substitutes on a real device, then port the remaining non-secret scripts. Hold the 18 secret variables pending the secrets decision.

Context: surfaced while bootstrapping level-script-sync against this repo (#46). The sync tool itself is correct — it syncs Level's script variables (output slots); it cannot invent an input mechanism Level doesn't have.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions