Rootless

Automate iOS, iPadOS, and macOS using plain JavaScript. Build custom widgets and interact directly with native APIs.

Download on the App Store
Rootless app interface

Plain JavaScript

Supports ES6. Scripts are stored as plain .js files on your disk.

Native APIs & Documentation

Integrate directly with iOS native APIs. Fully documented, available offline.

Siri Shortcuts

Trigger scripts via Siri. Present tables, websites, and custom HTML natively.

System Integration

Run from the Share Sheet, process inputs, and manage local files via Files.app.

x-callback-url

Communicate seamlessly and trigger workflows with other apps on your device.

Customizable & Ready

Tailor the code editor to your liking. Includes several example scripts to start immediately.

Documentation

Also inside the app, or as rootless.json.

LiveActivity

Lock Screen and Dynamic Island

The content is a widget tree, the same one Script.setWidget() takes. A Widget where a background and padding matter, a bare node like Text("38m") where they do not; both work anywhere content is accepted. Three limits come from iOS: starting an activity needs Rootless in the foreground, so a widget refresh or a Shortcut can update and end but not start; all four trees together must encode to under 4KB; and iOS ends an activity after eight hours. Activities are scoped per script, so all() and end() see only your own.

Properties

isSupportedBoolean

Whether this device can show Live Activities at all. False on Mac.

areEnabledBoolean

Whether the user has left them on for Rootless, in Settings.

Methods

LiveActivity.start(options)String

Starts one and returns its id. Options: title, content (a Widget or a node), island, staleAfter (Duration). island takes compactLeading, compactTrailing and minimal for the collapsed states, plus expanded: {leading, trailing, center, bottom} for the long-pressed one. Leading and trailing flank the camera cutout, center sits under it, bottom spans the full width. Without expanded, the banner is shown across the bottom and the band beside the cutout stays empty. Needs Rootless in the foreground.

  • options Any required
    • title String

      Names the activity. Not drawn. It identifies it in all().

    • content Any required

      The Lock Screen banner: a Widget, or a bare node when a background and padding are not wanted.

    • island Any

      The Dynamic Island. Leave it out and the island shows nothing.

      • compactLeading Any

        Left of the camera cutout when the island is collapsed. About 44pt wide, an icon or a few characters.

      • compactTrailing Any

        Right of the cutout, same size.

      • minimal Any

        A ~36pt circle, shown when another app also has an activity running and yours is squeezed.

      • expanded Any

        The long-pressed island, by region. Without it the banner is shown across the bottom.

        • leading Any

          Left of the cutout, level with it. Fill this and trailing or the band beside the camera stays empty.

        • trailing Any

          Right of the cutout.

        • center Any

          Under the cutout, between leading and trailing.

        • bottom Any

          Full width, beneath everything else.

    • staleAfter Duration

      How long before what is on screen counts as out of date. The activity stays up; all() starts reporting it as "stale".

returns The activity's id. Pass it to update() and end(). Worth putting in Storage: a later run has no other way back to it.

LiveActivity.update(id, options)

Replaces the content. Options: content, island, staleAfter, and alert ({title, body}) to make the update ring.

  • id String required

    From start(), or from all().

  • options Any required
    • content Any required

      Replaces the banner.

    • island Any

      Replaces the island. Same shape as in start().

      • compactLeading Any

        Left of the camera cutout when the island is collapsed. About 44pt wide, an icon or a few characters.

      • compactTrailing Any

        Right of the cutout, same size.

      • minimal Any

        A ~36pt circle, shown when another app also has an activity running and yours is squeezed.

      • expanded Any

        By region, as in start().

        • leading Any

          Left of the cutout, level with it. Fill this and trailing or the band beside the camera stays empty.

        • trailing Any

          Right of the cutout.

        • center Any

          Under the cutout, between leading and trailing.

        • bottom Any

          Full width, beneath everything else.

    • staleAfter Duration

      Resets the staleness clock.

    • alert Any

      Makes the update ring and light up the screen. Leave it out for a silent one.

      • title String

        Shown on devices that cannot display the activity itself, like a Watch.

      • body String

        The line under it.

LiveActivity.end(id, options)

Ends it. Options: content for a final frame, dismissAfter (Duration) to leave it on screen a while longer.

  • id String required

    From start(), or from all().

  • options Any required

    Optional, end(id) keeps whatever is on screen as the last frame.

    • content Any

      A final frame, if the last state is not the one to leave behind.

    • dismissAfter Duration

      Keeps it on the Lock Screen this much longer before it disappears. Default is the system's own timing.

LiveActivity.all()[Any]

This script's running activities.

returns One { id, title, state } per activity. state is "active", "stale" (still on screen but past staleAfter, so what it shows is out of date), "ended" or "dismissed". An activity the user swiped away is not in the list at all, so an empty result means the session is over.

LiveActivity.endAll()

Ends every activity this script started, immediately.

Examples

Start, update, end

var id = LiveActivity.start({
  title: "Laundry",
  content: new Widget({ child: Text("38 min left") }),
  island: {
    compactLeading: Text("🧺"),
    compactTrailing: Text("38m"),
    expanded: {
      leading: Text("🧺 Washing"),
      trailing: Text("38m"),
      bottom: ProgressBar({ value: 0.4 }),
    },
  },
});

LiveActivity.update(id, {
  content: new Widget({ child: Text("2 min left") }),
  alert: { title: "Almost done", body: "Two minutes left" },
});

LiveActivity.end(id, { dismissAfter: Duration.minutes(5) });

Only when it can work

if (!LiveActivity.areEnabled) {
  console.warn("Live Activities are off for Rootless.");
} else {
  LiveActivity.start({ title: "Timer", content: new Widget({ child: Text("Go") }) });
}