# Custom Scripts

The locations where WirePlumber searches for scripts is explained in Location of scripts.

Scripts are not loaded automatically; a component must be defined for them, and this component must be included in a profile. See Components & Profiles.

## Example 1: a standalone script

Let's assume that `~/.local/share/wireplumber/scripts/90-hello-world.lua` contains the following script:

    log = Log.open_topic ("hello-world")
    log.info ("Hello world")

In order for it to run, we'll define a component and include it in the default profile by including the following configuration (for example, in `~/.config/wireplumber/wireplumber.conf.d/90-hello-world.conf`):

    wireplumber.components = [
      {
        name = "90-hello-world.lua", type = script/lua
        provides = hello-world
      }
    ]

    wireplumber.profiles = {
      main = {
        hello-world = required
      }
    }

## Example 2: an event hook

A script that only runs once at startup is of limited use. Most of the logic in WirePlumber is written as event hooks, which run every time something happens in the graph. See Events and Hooks for the concepts and Events & Hooks for the API.

`~/.local/share/wireplumber/scripts/90-log-new-nodes.lua`:

    log = Log.open_topic ("s-log-new-nodes")

    SimpleEventHook {
      name = "example/log-new-nodes",
      interests = {
        EventInterest {
          Constraint { "event.type", "=", "node-added" },
          Constraint { "media.class", "#", "Stream/*" },
        },
      },
      execute = function (event)
        local node = event:get_subject ()
        log:info (node, "new stream: " ..
            tostring (node.properties ["node.name"]))
      end
    }:register ()

`~/.config/wireplumber/wireplumber.conf.d/90-log-new-nodes.conf`:

    wireplumber.components = [
      {
        name = "90-log-new-nodes.lua", type = script/lua
        provides = hooks.example.log-new-nodes
      }
    ]

    wireplumber.profiles = {
      main = {
        hooks.example.log-new-nodes = required
      }
    }

Note

The `wireplumber.components.rules` in the default configuration automatically add `requires = [ support.lua-scripting ]` to every `script/lua` component, and make components whose `provides` matches `hooks.*` load before the standard event source (the monitor scripts are excluded and load after it instead). Naming your feature `hooks.*` therefore ensures the hook is registered before any events are dispatched.

## Example 3: replacing a shipped hook

Registering a hook with the same name as an existing one does not replace it; both will run. To replace one of WirePlumber's own hooks, disable the feature that provides it in your profile and provide your own component instead.

For instance, `linking/find-default-target.lua` provides the feature `hooks.linking.target.find-default`. To substitute your own target selection logic:

    wireplumber.components = [
      {
        name = "90-my-find-target.lua", type = script/lua
        provides = hooks.example.find-target
      }
    ]

    wireplumber.profiles = {
      main = {
        hooks.linking.target.find-default = disabled
        hooks.example.find-target = required
      }
    }

To add to the selection chain rather than replace part of it, leave the stock hooks enabled and order your own hook against them with `before` and `after`; see Example 4: participating in target selection below for a worked example, and Existing Scripts for the list of hooks you can order against.

## Example 4: participating in target selection

When a stream needs to be linked, WirePlumber pushes a `select-target` event and a chain of hooks runs on it, each one trying to pick a target node for the stream. Every hook in that chain bypasses itself if a target has already been picked, so the first hook that finds a target wins; the hooks that follow it merely translate and link it. This is why the hook below returns early when `target` is already set: without that guard it would override a decision that a higher-priority hook has already made.

To take part in the chain, register another hook on the `select-target` event and order it relative to the stock hooks with `before` / `after`. The hook below runs `before` `linking/find-defined-target`, which makes it the first one to get a say, ahead of all of WirePlumber's own selection logic.

The `linking-utils` module provides `unwrap_select_target_event ()`, which unpacks the event into the objects the hook needs: the source event, the object manager, the session item being linked (`si`) along with its properties and flags, and the target picked so far (or `nil`). The chosen target is handed to the rest of the chain by storing it on the event with `event:set_data ("target", target)`.

`~/.local/share/wireplumber/scripts/90-find-user-target.lua`:

    lutils = require ("linking-utils")
    log = Log.open_topic ("s-linking")

    SimpleEventHook {
      name = "linking/find-user-target",
      before = "linking/find-defined-target",
      interests = {
        EventInterest {
          Constraint { "event.type", "=", "select-target" },
        },
      },
      execute = function (event)
        local source, om, si, si_props, si_flags, target =
            lutils:unwrap_select_target_event (event)

        -- bypass the hook if the target is already picked up
        if target then
          return
        end

        log:info (si, "in find-user-target")

        -- implement logic here to find a suitable target

        -- store the found target on the event,
        -- the next hooks will take care of linking
        event:set_data ("target", target)
      end
    }:register ()

`~/.config/wireplumber/wireplumber.conf.d/90-find-user-target.conf`:

    wireplumber.components = [
      {
        name = "90-find-user-target.lua", type = script/lua
        provides = hooks.example.find-user-target
      }
    ]

    wireplumber.profiles = {
      main = {
        hooks.example.find-user-target = required
      }
    }

As noted in Example 2 above, naming the feature `hooks.*` ensures that the component is loaded, and the hook registered, before any events are dispatched.

The stock hook that this one orders itself against must of course be enabled for the `before` key to have an effect; `linking/find-defined-target` is provided by `hooks.linking.target.find-defined`, which is part of the default profile. See Existing Scripts for the other hooks in the chain.

## Example 5: reading configuration and settings

Scripts read static configuration with Conf and runtime settings with Settings. Static configuration is read once at startup; settings can change while WirePlumber is running.

    -- static configuration, from a section in wireplumber.conf or a fragment
    config = {}
    config.rules = Conf.get_section_as_json ("example.rules", Json.Array {})

    -- a runtime setting, declared in wireplumber.settings.schema
    local enabled = Settings.get_boolean ("example.enabled")

    Settings.subscribe ("example.enabled", function ()
      enabled = Settings.get_boolean ("example.enabled")
    end)

## Debugging your script

Give your script its own log topic with `Log.open_topic ()` and use the `s-` prefix, matching the convention used by the shipped scripts. You can then enable just your messages:

    $ WIREPLUMBER_DEBUG=s-log-new-nodes:D wireplumber

Useful things to know while developing a script:

- `wpctl status` shows the current graph, which is the state your hooks are reacting to.

- `Debug.dump_table ()` pretty-prints a Lua table to the log.

- A script that does not need to run inside the daemon can be executed standalone against a running PipeWire with `wpexec`, which is much faster to iterate on. See Testing for examples.

- If your hook does not seem to run, check that its component is actually loaded — a profile entry naming a feature that does not exist is silently ignored. Raising the log level to `D` shows which components are loaded.

See also Events & Hooks for the full hook API and Existing Scripts for the hooks that ship with WirePlumber.