Authoring reference

Profile DSL reference

The Profile DSL is the Python API behind Pinman Profile Scripts. It declares a Profile's metadata, Interface, desired configuration, included Profiles, live controls, and ordered actions. Pinman evaluates the script to build the Profile shown in the UI and used by Preview, Apply, and Run.

This page specifies the DSL calls and their arguments. For a progressive introduction with complete examples, start with Profile Scripts.

Import and structure#

A Profile Script imports profile(), creates one Profile, and keeps the returned builder in a variable—conventionally p:

from pinman.profiles import profile

p = profile("Quiet Audio", zone_tags=["Audio"])
p.target("winrt:/Sound/Master/Volume", value=0.30)

Declaration order is meaningful. Declare the complete Interface first. Included content contributes where p.include() appears, later direct targets can override earlier ones, and steps run in order.

Common types#

TypeAccepted values
ProfileValuestr, int, float, or bool
Item locationA tracked Pinman location, which may contain Project or device-role bindings.
itemtype"auto" by default, or an explicit Schema itemtype for an advanced override.
Input type"text", "number", "select", or "boolean"

Profile values cannot be bytes. Use the text representation expected by the Item adapter—for example, Registry binary data uses hex: text.

profile()#

profile(
    name: str,
    zone_tags: list[str] | None = None,
    description: str | None = None,
    software_refs: list[str] | None = None,
    status: str = "in_sync",
) -> ProfileBuilder

Creates the Profile and returns the builder used for the remaining declarations.

ArgumentDescription
nameReader-facing Profile name.
zone_tagsZones associated with the Profile.
descriptionBrief explanation of the configuration or action.
software_refsSoftware Catalog entries associated with the Profile.
statusInitial builder metadata. Ordinary Profile Scripts leave the default; Pinman's comparison determines the operational status.

Call profile() once in each script. Its return value is the only public object needed to declare Profile content.

p = profile(
    "Recording Session",
    description="Prepare audio and display settings for recording",
    zone_tags=["Audio", "Display"],
    software_refs=["example:recorder"],
)

Interface#

p.input()#

p.input(
    name: str,
    label: str,
    type: Literal["text", "number", "select", "boolean"] = "text",
    default: ProfileValue = REQUIRED,
    choices: dict[str, str] | None = None,
    choice_sources: dict[str, str] | None = None,
    help: str | None = None,
    min_value: float | None = None,
    max_value: float | None = None,
    step: float | None = None,
    group: str | None = None,
    order: int | None = None,
    widget: str | None = None,
) -> None

Declares one public Interface input. All inputs must be declared before the script reads p.inputs or adds Profile content.

REQUIRED is descriptive notation in this signature, not a name to import: omit default to make the input required.

ArgumentDescription
namePublic Python identifier used by p.inputs.<name>. It cannot begin with _.
labelReader-facing label.
typeInput type; defaults to "text".
defaultDefault value. Omit it to require the person using the Profile to supply a value.
choicesFor select, a mapping of stored values to reader-facing labels.
choice_sourcesOptional provenance labels for select choices. Authoring tools use this to retain where a choice came from; it does not change runtime behavior.
helpExplanatory text displayed with the input.
min_value, max_valueInclusive bounds for a number input.
stepPositive increment presented for a number input.
groupOptional visual grouping name.
orderOptional position within its group.
widgetOptional visual-editor widget preference.

choices and choice_sources apply only to select. Numeric bounds and step apply only to number; bounds and defaults must be finite, and step must be greater than zero.

p.input(
    "mode",
    label="Audio mode",
    type="select",
    default="game",
    choices={"game": "Game", "quiet": "Quiet"},
    help="Choose the master-audio configuration.",
)

p.inputs.<name>#

value: ProfileValue = p.inputs.<name>

Returns the validated runtime value of a declared input. A supplied value takes precedence over its default. Reading a required input without supplying a value stops evaluation and identifies the missing input.

Input typeReturned type
textstr
selectstr choice key
numberint or float
booleanbool

Reading p.inputs closes the Interface, so no later p.input() calls are accepted.

mode = p.inputs.mode
volume = 0.80 if mode == "game" else 0.30

Desired configuration#

p.target()#

p.target(
    location: str,
    value: ProfileValue,
    itemtype: str = "auto",
    enabled: bool = True,
    authored_from: str | None = None,
) -> None

Declares the desired value of one tracked property.

ArgumentDescription
locationWritable Item location.
valueDesired value.
itemtypeItemtype override. Leave "auto" to use the active Schema.
enabledWhen False, retain the declaration but exclude it from comparison and Apply.
authored_fromOptional authoring-provenance label, such as a Capture name. It does not affect Apply.

Outside a step, the target participates in comparison and Apply. Inside p.step(), the same call declares an ordered write that always runs and does not represent lasting desired state.

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

p.remove()#

p.remove(
    location: str,
    itemtype: str = "auto",
    enabled: bool = True,
    authored_from: str | None = None,
) -> None

Declares that one tracked property should be absent.

ArgumentDescription
locationWritable Item location to remove.
itemtypeItemtype override; normally "auto".
enabledWhen False, retain the declaration without comparing or applying it.
authored_fromOptional authoring-provenance label.

Inside p.step(), removal becomes an ordered stage action rather than lasting desired state.

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

p.file()#

p.file(
    location: str,
    *,
    asset: str,
    itemtype: str = "auto",
    enabled: bool = True,
    authored_from: str | None = None,
) -> None

Declares that a tracked file or directory should match an asset stored with the Profile.

ArgumentDescription
locationDestination file or directory location. Do not include a structured-file # data path.
assetRelative path below the Profile's asset directory.
itemtype"auto", "file", or "directory". With "auto", Pinman infers the kind from the asset.
enabledWhen False, exclude the file target from comparison and Apply.
authored_fromOptional authoring-provenance label.

For presentation.py, assets live under presentation/ beside the script:

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

p.file() cannot appear inside p.step().

p.guided()#

p.guided(
    location: str,
    value: ProfileValue,
    *,
    software: str,
    itemtype: str = "auto",
    instructions: list[str] | None = None,
    verify: bool = True,
    enabled: bool = True,
) -> None

Declares desired state that a person completes through another program.

ArgumentDescription
locationTracked Item location.
valueDesired value.
softwareSoftware Catalog ID for the program used to make the change.
itemtypeItemtype override; normally "auto".
instructionsOrdered steps shown to the person. When omitted, Pinman can resolve guidance from reference data.
verifyRe-read the Item after the person completes the steps; defaults to True.
enabledWhen False, skip this guided target.
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.",
    ],
)

p.guided() cannot appear inside p.step().

Read current state#

p.read()#

p.read(
    location: str,
    itemtype: str = "auto",
) -> ProfileValue | None

Makes a current captured value available to Profile logic. The read is not a target: it is not compared, applied, or included in Profile status.

ArgumentDescription
locationItem location to read.
itemtypeItemtype override; normally "auto".

Pinman evaluates a script using p.read() in two passes. The first pass records the requested locations; the second returns their captured values. The call returns None when no value is available or when an authoring command evaluates the script without a Project database session. Handle that case explicitly.

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

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

Composition#

p.include()#

p.include(
    profile_path: str,
    inputs: dict[str, object] | None = None,
    input_map: dict[str, str] | None = None,
) -> None

Evaluates another Profile Script and contributes its content at the point of the call. The path is relative to the containing Profile.

ArgumentDescription
profile_pathRelative path to the included Profile Script.
inputsFixed, private values for child inputs.
input_mapMapping from a child input name to an explicitly declared input in the containing Profile.

A child input named in neither mapping is promoted into the containing Profile's Interface under the same name. Compatible inputs promoted by multiple children share one Interface field.

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

p.include(
    "audio.py",
    input_map={"mode": "audio_mode"},
)
p.include("display.py", inputs={"resolution": "high"})

The same child input cannot be both fixed and mapped. Mapped input types and ranges must be compatible. Circular includes are rejected, and p.include() cannot appear inside p.step().

Included direct, read, guided, and file targets retain their source identity. Included controls and ordered steps also contribute to the containing Profile.

p.drop()#

p.drop(predicate: Callable[[Target], bool]) -> None

Removes direct targets collected before the call when predicate(target) returns True. It is useful after p.include() when the containing Profile replaces selected child targets.

The predicate can inspect these public target fields:

FieldMeaning
locationAuthored Item location.
itemtypeAuthored itemtype or "auto".
target_valueDesired value.
action"set" or "remove".
enabledWhether the target participates in comparison and Apply.
authored_fromOptional provenance label.
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() declarations. File, guided, read, control, and step declarations are separate collections.

Live controls#

p.control()#

p.control(
    location: str | None = None,
    *,
    input: str | None = None,
    group: str = "",
    order: int = 0,
    widget: WidgetKind | None = None,
    step: float | None = None,
    label: str | None = None,
    value: ProfileValue | None = None,
) -> None

Places a tracked property or declared Interface input on a live control surface. A control reads and writes immediately; it does not represent desired state and is not part of comparison or Apply.

Supply exactly one of location or input.

ArgumentDescription
locationProperty location for a property control.
inputName of a previously declared Interface input.
groupVisual group label.
orderPosition within the group. Equal values retain declaration order.
widgetOptional widget override.
stepNumeric increment override for a property control.
labelReader-facing label override.
valueFixed value written by a property button.

WidgetKind is one of "slider", "toggle", "stepper", "select", "text", "color", "duration", or "button". Omitted property-control presentation fields come from the Schema Item.

An input control uses the input's range and step metadata, so it does not accept step or value:

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

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

The same property may be declared more than once with different widgets.

Ordered actions#

p.step()#

p.step(hold: float = 0.0) -> StepSpec

Creates one stage in an ordered action. Use the returned object as a context manager:

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

with p.step():
    p.invoke("rest:/system/shutdown")
ArgumentDescription
holdSeconds to wait after this stage and before the next. It must be zero or greater.

Steps run in declaration order after any ordinary direct targets. Targets and removals inside a step are stage directions: Pinman writes them every time the Profile runs, and they do not contribute to Profile status.

Steps form a flat sequence and cannot be nested. Use p.step() only with with; p.guided(), p.file(), and p.include() are not valid inside a step.

p.invoke()#

p.invoke(location: str) -> None

Invokes the tracked REST command at location when the Profile runs. A command has no value and does not participate in comparison.

Inside p.step(), the command joins that stage. A bare p.invoke() creates a one-command step.

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

Run a Profile containing steps or commands with pinman profile run.

Apply hooks#

p.on_before_apply#

@p.on_before_apply
def before(targets: list[Target]) -> None:
    ...

Registers one function to run immediately before Pinman begins the Profile's writes. The function receives the declared direct targets. Raising an exception stops Apply before those writes begin.

The decorator returns the original function. Registering another before hook replaces the earlier one.

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

p.on_after_apply#

@p.on_after_apply
def after(result: ApplyResult) -> None:
    ...

Registers one function to run when Apply finishes, including an Apply that stops before writing. Completed writes and their history are recorded before the hook runs. The function receives an ApplyResult with these commonly useful fields:

FieldTypeMeaning
successboolWhether Apply completed successfully.
changed_countintExisting values changed.
added_countintValues added.
removed_countintValues removed.
unchanged_countintTargets already at the requested value.
failed_countintTargets that failed.
detailslistStructured failure and warning details.
history_record_idint | NoneApply-history record used by Undo.

The decorator returns the original function. Registering another after hook replaces the earlier one. If an after hook raises, completed writes remain applied and available to Undo.

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

Validation errors#

The DSL raises an error while evaluating the script when a declaration is not coherent. Common examples include:

  • reading an undeclared or missing required input;
  • declaring an input after Profile content;
  • supplying an unknown select choice or an out-of-range number;
  • fixing and mapping the same child input;
  • including Profiles in a cycle;
  • giving p.control() both location and input; and
  • nesting steps or using a negative hold.

Use the authoring commands to catch these errors without changing the Machine:

pinman profile authoring check path\to\profile.py
pinman profile authoring lint path\to\profile.py

Supply required or non-default Interface values with repeated --input name=value options. check reports the resulting declarations; lint also checks writable target structure. Preview is the Project-aware validation step that resolves bindings, Schema Items, and write support.