Authoring

Profile Scripts

A Profile is a named, reusable recipe for putting part of a Machine into a configuration you want. The simplest Profile sets one value. A larger one can choose among value sets, calculate values, reuse other Profiles, replace whole files, or coordinate an ordered sequence of actions.

Profile Scripts are Python files built with Pinman's small Profile DSL. This guide assumes you can read function calls, variables, and if statements. If those are new to you, the official Python introduction provides enough background to follow the examples here.

A first Profile#

This complete Profile asks Pinman to mute Windows master audio:

from pinman.profiles import profile

p = profile(
    "Master Audio — Mute",
    description="Mute Windows master audio",
    zone_tags=["Audio"],
)

p.target("winrt:/Sound/Master/Mute", value=True)

The script has three parts:

  1. Import profile, the entry point to the DSL.
  2. Create one Profile and give it a name and optional metadata.
  3. Declare the desired value with p.target().

The target location must identify a writable Item in the active Schema. Pinman uses that Item to determine how to read, compare, and write the value. The target's itemtype therefore defaults to auto; an explicit itemtype is only needed for unusual authoring cases.

A Profile describes desired state. Loading the script does not change the Machine. Preview compares its targets with the current Machine, and Apply makes the approved changes.

The script and visual editor#

The Python file and Pinman's Profile editor are two views of the same Profile. The editor reads the script and presents its common structures in four tabs:

TabWhat it shows
ContentsIncluded Profiles, choices, and the order in which content contributes to the result.
TargetsThe tracked locations and values produced by the Profile.
InterfaceThe inputs declared with p.input().
ScriptThe complete Python source.

For structures the visual editor recognizes, edits in the panels update the Python and edits in the Python update the panels. The script remains the full, portable definition. More advanced Python continues to work even when a panel cannot safely rewrite it; the editor marks that content as source-owned.

Create and check a script#

The standard Pinman installation includes the Profile DSL, its Python runtime, and all pinman profile authoring commands. You do not need a separate Python installation, compiler, SDK, source checkout, or developer environment. Use Pinman's Script editor or any text editor to edit the .py file; run check and lint from PowerShell with the regular pinman command. The authoring commands run locally and do not require the Pinman API server or browser UI.

Generate a starter file in the active Project's Profiles directory:

pinman profile authoring scaffold "Master Audio — Mute"

You can also create a .py file yourself. After adding, removing, or renaming Profile files outside Pinman, reconcile them with the Project:

pinman profile sync

Use the authoring commands while you work:

pinman profile authoring check path\to\master_audio_mute.py
pinman profile authoring lint path\to\master_audio_mute.py
pinman profile preview "Master Audio — Mute"

check executes the script and shows its Interface and resulting content. lint checks its declarations and writable target structure. Neither authoring command changes the Machine. preview resolves the targets against the active Project's Schema and shows what applying the Profile would change. Preview is the final check before Apply.

Profile metadata (profile())#

The profile() call creates the one Profile defined by the file:

p = profile(
    "Video Recording",
    description="High-quality settings for a recording session",
    zone_tags=["Production"],
    software_refs=["example:recorder"],
)
ArgumentPurpose
nameReader-facing Profile name.
descriptionBrief explanation of the configuration or action.
zone_tagsZones associated with the Profile.
software_refsSoftware Catalog entries associated with the Profile.

Each script calls profile() once. Put Interface declarations immediately after it, followed by the content that uses them.

Direct targets (p.target())#

p.target() declares that one tracked property should have one value:

p.target(
    "fs:/C/Example/recorder.ini#capture/frame_rate",
    value=60,
)

p.target(
    "winreg:/HKEY_CURRENT_USER/Software/Example/ShowTips",
    value=False,
)

p.target("winrt:/Sound/Master/Volume", value=0.75)

Profile values are strings, integers, floating-point numbers, or booleans. The Item's Schema datatype and adapter determine how Pinman validates and stores the value.

The three locations in the example above address different realms, but the Profile code is the same. Copy locations from Explore or another Pinman view rather than assembling them by hand. Schema Items explains realm paths and settings inside structured files.

Remove a target (p.remove())#

p.remove() makes absence the desired state for a property:

p.remove("fs:/C/Example/recorder.ini#capture/obsolete_option")

Keep an optional target disabled#

enabled=False is useful when a reusable Profile describes an optional component, such as a second display or a device that is not installed on every Machine. The declaration remains visible in the script and Preview, but a missing optional Item cannot block Apply because the target does not participate in comparison or Apply:

p.target("winrt:/Sound/Master/Mute", value=True, enabled=False)

Portable locations#

A Profile can use the same bindings as the rest of the Project. A global binding keeps an installation path portable:

p.target("{{RECORDER_ROOT}}/recorder.ini#capture/frame_rate", value=60)

A device-role binding targets the device assigned to that role on each Machine:

p.target(
    "winrt:/Display/Monitors/{{PlayfieldMonitor.os_id}}/RefreshRate",
    value=120,
)

Pinman resolves these placeholders before Preview or Apply. See Schema bindings and device roles for their declarations.

Add a Profile Interface (p.input())#

An Interface defines the input values a person can supply when previewing or applying a Profile. Declare every input before reading it or adding Profile content:

p.input(
    "mode",
    label="Audio mode",
    type="select",
    default="game",
    choices={"game": "Game", "quiet": "Quiet"},
)

mode = p.inputs.mode

p.input() declares the Interface field. It does not return its runtime value. Read that value from p.inputs.<name> after all inputs have been declared.

Use ordinary Python to choose target values:

if mode == "game":
    volume = 0.80
    mute = False
else:
    volume = 0.30
    mute = False

p.target("winrt:/Sound/Master/Volume", value=volume)
p.target("winrt:/Sound/Master/Mute", value=mute)

Preview or apply a non-default choice with --input:

pinman profile preview "Audio Modes" --input mode=quiet

Input types#

TypeValueUseful fields
textStringdefault, help
numberInteger or floatdefault, min_value, max_value, step
selectOne choice keydefault, choices
booleanTrue or Falsedefault, help

Omit default to make an input required. The person must then supply a value in the UI or with --input before Pinman can determine the Profile's content for Preview or Apply. Input names must be public Python identifiers, because each one is read as an attribute of p.inputs.

The straightforward select pattern above is especially useful: Pinman's visual editor can present each choice and its resulting target values while the Script remains ordinary Python.

Complete example: a selectable audio Profile
from pinman.profiles import profile

p = profile(
    "Audio Modes",
    description="Switch between game and quiet master-audio settings",
    zone_tags=["Audio"],
)

p.input(
    "mode",
    label="Audio mode",
    type="select",
    default="game",
    choices={"game": "Game", "quiet": "Quiet"},
)

mode = p.inputs.mode

if mode == "game":
    volume = 0.80
else:
    volume = 0.30

p.target("winrt:/Sound/Master/Volume", value=volume)
p.target("winrt:/Sound/Master/Mute", value=False)

Calculate target values#

A Profile Script can use functions, arithmetic, dictionaries, and other normal Python constructs. This example calculates a 16:9 height from an Interface value:

p.input(
    "width",
    label="Display width",
    type="number",
    default=1920,
    min_value=640,
    max_value=7680,
)

width = int(p.inputs.width)
height = round(width * 9 / 16)

p.target("{{DISPLAY_CONFIG}}#Display/Width", value=width)
p.target("{{DISPLAY_CONFIG}}#Display/Height", value=height)

Complex Python remains valid even when the visual editor cannot safely rewrite it. The Script tab remains the source of truth, and Preview shows the resulting targets for the chosen inputs.

Read current state (p.read())#

p.read() makes a current captured value available to the script. It is useful when a target depends on another setting rather than only on Interface inputs:

current = p.read("winrt:/Sound/Master/Volume")

if current is not None:
    quieter = max(0.0, float(current) - 0.10)
    p.target("winrt:/Sound/Master/Volume", value=quieter)

The read is not a target and does not participate in Profile status or Apply. Pinman loads a script that uses p.read() in two passes: first to collect the locations, then again with their captured values. It can return None when no value is available and when authoring commands load the script without a database session, so handle that case explicitly.

Reuse other Profiles (p.include())#

p.include() links another Profile into the current one. The included Profile remains a separate file and recognizable unit; later edits to it are reflected the next time Pinman evaluates the containing Profile.

p.include("audio.py")
p.include("display.py")

Includes contribute content at the point where they appear. Declarations later in the script take precedence when more than one contribution addresses the same target. Put an intentional direct override after the includes:

p.include("audio.py")
p.include("display.py")

p.target("winrt:/Sound/Master/Mute", value=False)

In the visual editor, the Contents tab shows this contribution order, identifies duplicate targets, and explains which later contribution wins. Preview shows the resulting target values before Apply.

Child inputs#

An included Profile may have its own Interface. p.include() offers three ways to connect it to the containing Profile:

DeclarationResult
p.include("audio.py")Compatible child inputs are promoted into the containing Profile under the same names.
p.include("audio.py", inputs={"mode": "quiet"})The child input is fixed to a private value.
p.include("audio.py", input_map={"mode": "audio_mode"})The child input receives the value of an explicitly declared containing-Profile input.

The mapped input must accept every value exposed by the containing input. A child input cannot be both fixed and mapped.

Choose whole Profiles#

A select input can choose which linked Profile participates:

p.input(
    "display",
    label="Display configuration",
    type="select",
    default="high",
    choices={"high": "High resolution", "low": "Low resolution"},
)

display = p.inputs.display

if display == "high":
    p.include("display_high.py")
else:
    p.include("display_low.py")

For a choice-specific include, fix or map all of the child's inputs. This keeps the containing Profile's Interface stable regardless of the selected branch.

Complete example: common, selected, and direct content
from pinman.profiles import profile

p = profile(
    "Workstation Modes",
    description="Common audio with a selected display setup",
    zone_tags=["Audio", "Display"],
)

p.input(
    "display",
    label="Display configuration",
    type="select",
    default="high",
    choices={"high": "High resolution", "low": "Low resolution"},
)

display = p.inputs.display

# Used for every choice.
p.include("audio.py", inputs={"mode": "game"})

# One linked Profile participates for the selected choice.
if display == "high":
    p.include("display_high.py")
else:
    p.include("display_low.py")

# Direct intent comes last and can override included content.
p.target("winrt:/Sound/Master/Mute", value=False)

Filter included targets (p.drop())#

p.drop() removes matching direct targets collected so far. Add replacements afterward:

p.include("base.py")
p.drop(lambda target: target.location == "winrt:/Sound/Master/Volume")
p.target("winrt:/Sound/Master/Volume", value=0.60)

p.drop() filters ordinary p.target() and p.remove() entries. It does not filter file, guided, read, or control declarations.

Whole-file targets (p.file())#

p.file() makes an entire tracked file or directory match an asset stored with the Profile:

p.file(
    "fs:/C/Example/layout.json",
    asset="layout.json",
)

Assets live in a directory beside the script with the same base name:

profiles/
  presentation.py
  presentation/
    layout.json

The destination must be tracked by the Schema. Preview compares the current file with the asset; Apply replaces it, and Undo retains the previous content. For selectable assets, use choice-specific paths such as asset="high/layout.json" and asset="low/layout.json".

Guided targets (p.guided())#

p.guided() declares desired state that a person changes through another program rather than through a direct Pinman write:

p.guided(
    "winrt:/GPU/Global/PowerManagementMode",
    "PreferMax",
    software="nvidia:app",
    instructions=[
        "Open the NVIDIA app.",
        "Open the global graphics settings.",
        "Set Power management mode to Prefer maximum performance.",
    ],
)

The software value is a Software Catalog ID. Instructions may be supplied by the script or resolved from reference data. With verify=True, the default, Pinman reads the setting again after the person completes the steps. Guided targets participate in comparison like direct targets.

Live controls (p.control())#

p.control() places a property or an Interface input on a live control surface. A control reads and writes the value immediately; it carries no desired state and does not participate in Profile status or Apply.

p.control(
    "rest:/audio/volume",
    group="Sound",
    order=10,
    widget="slider",
)

p.control(
    input="mode",
    group="Configuration",
    order=20,
)

For a property control, omitted widget, step, and label values come from the Schema Item. An input control uses the metadata declared by p.input().

Ordered actions (p.step(), p.invoke())#

Desired-state targets describe values that should remain true. Some Profiles instead represent an ordered action: write a temporary value, wait, then invoke a command. Put those operations in p.step() blocks:

with p.step(hold=10):
    p.target("rest:/display/message", value="Shutting down in 10 seconds")

with p.step():
    p.invoke("rest:/system/shutdown")

Steps run in declaration order. hold is the pause after that step and before the next. A failed step stops the run. Targets inside a step are stage directions, so they are always written and do not represent lasting desired state.

Run a sequenced Profile with profile run, not profile apply:

pinman profile run "Shutdown Countdown" --allow-writes

Always use p.step() as a with block. Steps cannot be nested, and p.guided(), p.file(), and p.include() cannot appear inside one. A bare p.invoke() creates a one-command step.

Apply hooks (p.on_before_apply, p.on_after_apply)#

Hooks run code immediately before or after Apply:

@p.on_before_apply
def before(targets):
    print(f"Applying {len(targets)} direct targets")


@p.on_after_apply
def after(result):
    print(f"Changed {result.changed_count}; failed {result.failed_count}")

Use hooks for deliberate apply-time integration such as logging or notifying another service. They do not change the target list. An error in the after hook does not roll back an Apply that Pinman has already recorded.

What the visual editor can preserve#

Profile Scripts remain Python, so the visual editor is intentionally conservative. It can safely edit common literal structures, including:

  • simple direct targets;
  • declared inputs;
  • one straightforward select-controlled set of target values;
  • unconditional included Profiles; and
  • one straightforward select-controlled choice of included Profiles.

Loops, calculated paths, nested conditions, dynamically generated Interface fields, and other arbitrary Python still run normally. Pinman presents those parts as source-owned rather than pretending it can round-trip them visually. Use the Script editor for those constructs and Preview to inspect the resulting Interface and targets.

DSL quick reference#

For complete signatures, arguments, return values, and constraints, see the Profile DSL reference.

DeclarationPurposeRole in the Profile
profile(...)Define the Profile and its metadata.Definition
p.input(...)Declare one public Interface value.Interface
p.inputs.nameRead a validated Interface value.Profile logic
p.target(...)Set one tracked property.Desired configuration
p.remove(...)Make one tracked property absent.Desired configuration
p.file(...)Replace a tracked file or directory with an asset.Desired configuration
p.guided(...)Describe a manually completed target.Desired configuration
p.read(...)Read captured Machine state for Python logic.Profile logic
p.include(...)Link another Profile and contribute its content.Composition
p.drop(...)Remove matching direct targets collected so far.Composition
p.control(...)Place a property or input on a live control surface.Live control
p.step(...)Group ordered writes and commands.Ordered action
p.invoke(...)Invoke a tracked REST command during a run.Ordered action

Authoring workflow#

Before sharing a Profile:

  1. Run pinman profile authoring check for its default Interface values and for each important choice.
  2. Run pinman profile authoring lint.
  3. Preview it against a representative Project and Machine.
  4. Review duplicate-target warnings and the final value chosen for each target.
  5. Exercise Apply, verification, Undo, and any required Savepoint protection.
  6. Review every included Profile and other imported Python source.

The complete command syntax is available in the CLI reference.