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:
- System variables —
level_*, always available (hostname, group path, IPs, tags…). No setup.
- 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.
- 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 only — Automation 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.
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_caseruntime variables into the script before execution:Level does not. On Level,
$profile_to_keepis 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:Three sources, and only three:
level_*, always available (hostname, group path, IPs, tags…). No setup.custom_field.reference, "How the field is referenced in automation scripts", examplecf_region. Values settable global → group → device.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 only — Automation Variables: "Script variables are for capturing output, not supplying input."
Verified facts
createCustomField(input:{name!, adminOnly, value, description}).referenceis derived, not settable: creating a field namedZZ Test Profile To Keepyieldedreference: cf_zz_test_profile_to_keep. So the token iscf_+ snake_case(name). (Created and deleted during testing; the tenant currently has 0 custom fields.){{cf_x}}substitute. Syntax is documented andreferenceis spec'd, but end-to-end substitution is inference until it runs once. Pilot one script before porting 43.The port
The existing empty/placeholder guard still works and should be kept.
18 of the 84 variables are secrets (tokens, passwords, API keys). I checked:
secrets/vault/credentialsresource in Level's REST v2 spec or its GraphQL schema.CustomFieldtype exposesid, name, reference, adminOnly, value— nosecret,masked,encrypted, orsensitivefield, and no field-type enum. The docs never mention masking.admin_onlyis access control, not protection: "This field's value can only be viewed or edited by an account administrator."That is materially weaker than a SuperOps runtime value typed at launch and never stored. Putting
CloudflaredTunnelToken,NewPasswordorACCOUNT_KEYinto 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
cloudflared_installCloudflaredTunnelToken{{cf_cloudflared_tunnel_token}}delprof2_old_profiles_deletedays_old{{cf_days_old}}delprof2_selected_profile_keepprofile_to_keep{{cf_profile_to_keep}}delprof2_specific_profile_deleteprofile_to_delete{{cf_profile_to_delete}}directory_cleanupruntime_folderPath{{cf_runtime_folder_path}}directory_cleanupruntime_days{{cf_runtime_days}}directory_size_analyzeYourTargetDirectoryHere{{cf_your_target_directory_here}}dns_set_providerDnsProviderSelection{{cf_dns_provider_selection}}files_rename_snake_caseYourTargetPathHere{{cf_your_target_path_here}}gcpw_installYourDomainsHere{{cf_your_domains_here}}gcpw_installYourEnrollmentTokenHere{{cf_your_enrollment_token_here}}huntress_install_macosACCOUNT_KEY{{cf_a_c_c_o_u_n_t_k_e_y}}huntress_install_macosORG_KEY{{cf_o_r_g_k_e_y}}huntress_install_macosINSTALL_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_createNewAdminUsername{{cf_new_admin_username}}local_user_admin_setupNewAdminUsername{{cf_new_admin_username}}local_user_admin_setupNewAdminFullName{{cf_new_admin_full_name}}local_user_admin_toggleTargetUsername{{cf_target_username}}local_user_createNewUsername{{cf_new_username}}local_user_createNewPassword{{cf_new_password}}local_user_deleteUsernameToDelete{{cf_username_to_delete}}local_user_disableUsernameToDisable{{cf_username_to_disable}}maintenance_rebootgraceful_true_or_false{{cf_graceful_true_or_false}}maintenance_rebootmaxuptimedays{{cf_maxuptimedays}}mrt_scanQuickOrFull{{cf_quick_or_full}}msi_url_installMSIURL{{cf_m_s_i_u_r_l}}nirsoft_uninstall_viewYourAppPatternHere{{cf_your_app_pattern_here}}password_files_searchGoogleChatWebhook{{cf_google_chat_webhook}}password_files_tickets_searchSuperOpsApiKey{{cf_super_ops_api_key}}password_files_tickets_searchGoogleChatWebhook{{cf_google_chat_webhook}}prinstall_addYourPrinterIpHere{{cf_your_printer_ip_here}}prinstall_addYourUsbQueueNameHere{{cf_your_usb_queue_name_here}}prinstall_addYourDriverNameHere{{cf_your_driver_name_here}}prinstall_addYourPrinterNameHere{{cf_your_printer_name_here}}prinstall_driver_addYourDriverTargetHere{{cf_your_driver_target_here}}prinstall_driver_addYourDriverNameHere{{cf_your_driver_name_here}}prinstall_driver_removeYourDriverTargetHere{{cf_your_driver_target_here}}prinstall_driversYourIpHere{{cf_your_ip_here}}prinstall_driversYourModelHere{{cf_your_model_here}}prinstall_idYourIpHere{{cf_your_ip_here}}prinstall_removeYourPrinterTargetHere{{cf_your_printer_target_here}}prinstall_scanYourSubnetHere{{cf_your_subnet_here}}prinstall_scanScanModeAllNetworkUsb{{cf_scan_mode_all_network_usb}}prinstall_setupInstallOrUninstall{{cf_install_or_uninstall}}prinstall_trust_codesignCertPath{{cf_cert_path}}restic_b2_backup_installB2KeyId{{cf_b2_key_id}}restic_b2_backup_installB2AppKey{{cf_b2_app_key}}restic_b2_backup_installB2BucketName{{cf_b2_bucket_name}}restic_b2_backup_installRepoPassword{{cf_repo_password}}restic_b2_backup_installClientName{{cf_client_name}}rustic_installYourBackendType{{cf_your_backend_type}}rustic_installYourBackendPath{{cf_your_backend_path}}rustic_installYourRepoPassword{{cf_your_repo_password}}rustic_installYourClientName{{cf_your_client_name}}rustic_installYourBackendKeyId{{cf_your_backend_key_id}}rustic_installYourBackendAppKey{{cf_your_backend_app_key}}rustic_installYourBackendRegion{{cf_your_backend_region}}rustic_installYourBackupPaths{{cf_your_backup_paths}}rustic_installYourExcludePatterns{{cf_your_exclude_patterns}}rustic_installYourBackupHour{{cf_your_backup_hour}}rustic_install_unixYourBackendType{{cf_your_backend_type}}rustic_install_unixYourBackendPath{{cf_your_backend_path}}rustic_install_unixYourRepoPassword{{cf_your_repo_password}}rustic_install_unixYourClientName{{cf_your_client_name}}rustic_install_unixYourBackendKeyId{{cf_your_backend_key_id}}rustic_install_unixYourBackendAppKey{{cf_your_backend_app_key}}rustic_install_unixYourBackendRegion{{cf_your_backend_region}}rustic_install_unixYourBackupPaths{{cf_your_backup_paths}}rustic_install_unixYourExcludePatterns{{cf_your_exclude_patterns}}rustic_install_unixYourBackupHour{{cf_your_backup_hour}}rustic_restoreYourSnapshotId{{cf_your_snapshot_id}}rustic_restoreYourRestorePath{{cf_your_restore_path}}rustic_restoreYourDestination{{cf_your_destination}}rustic_restore_unixYourSnapshotId{{cf_your_snapshot_id}}rustic_restore_unixYourRestorePath{{cf_your_restore_path}}rustic_restore_unixYourDestination{{cf_your_destination}}sentinelone_installSiteToken{{cf_site_token}}superops_service_restartYourServiceFilterHere{{cf_your_service_filter_here}}terminated_user_lockoutTerminatedUsername{{cf_terminated_username}}winget_package_installPackageName{{cf_package_name}}workstation_rename_autoYourApiKeyHere{{cf_your_api_key_here}}workstation_rename_manualYourApiKeyHere{{cf_your_api_key_here}}workstation_rename_manualYourCustomClientHere{{cf_your_custom_client_here}}workstation_rename_manual_macosYourCustomClientHere{{cf_your_custom_client_here}}Asset-field placeholders (no custom field; use system variables)
workstation_rename_auto:YourAssetIdHere,YourAssetNameHere,YourClientNameHereworkstation_rename_auto_macos:YourClientNameHereworkstation_rename_manual:YourAssetIdHere,YourAssetNameHere,YourClientNameHereworkstation_rename_manual_macos:YourClientNameHereAsset-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.