# JavaScript injection


> Attach a script to a web content item and the player runs it after the page loads: dismissing banners, hiding chrome, or completing a login form.
Attach JavaScript to a web content item and the player runs it against the page once the page has finished loading, every time that item plays.

It is the escape hatch for pages you do not control. A cookie banner covering the dashboard, a navigation bar wasting the top third of the screen, a login form that basic auth cannot satisfy: all of them are a few lines of DOM manipulation, and none of them are a Screenly setting.

> [!NOTE]
> There is no dashboard control for this. It is set through the API, the CLI, or the MCP server, and nowhere else.

## Setting it

The CLI takes a local file or a URL, which makes the script itself a reviewable file in your repository rather than a blob pasted into a form.

```bash
screenly asset inject-js <asset-uuid> ./dismiss-banner.js
```

```bash
screenly asset inject-js <asset-uuid> https://example.com/scripts/login.js
```

Over the API it is the `js_injection` field on the asset.

```bash
curl -X PATCH \
  'https://api.screenlyapp.com/api/v4.1/assets?id=eq.<asset-uuid>' \
  -H "Authorization: Token $SCREENLY_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"js_injection": "document.querySelector(\".cookie-banner\")?.remove();"}'
```

An assistant can set it too, through the MCP server's asset update tool. See [Tools](/docs/developers/mcp/reference/tools/).

## When it runs

The script runs after the page reports itself fully loaded, so the DOM is in place and you do not need to wait for `DOMContentLoaded` yourself. It runs on every load, not once per item, so a script that logs in runs again each time the page comes round in the playlist.

It has no way to signal back. Whatever the script returns is logged on the device and discarded; the page plays regardless of whether your code threw.

## Write once, read never

`js_injection` is write-only. Reading the asset back does not return your code, it returns a placeholder:

```json
{ "js_injection": "<hidden; sha256 a1b2c3...>" }
```

An empty string means nothing is set. The placeholder is derived from the script, so it changes when the script changes, which is enough to detect drift in a pipeline. It is not enough to recover the script, so keep the source in version control. There is no way to read it back out of Screenly.

## Limits

The field holds up to 65,536 characters. That is generous for DOM manipulation and small for a bundle: if you are approaching it, host the real script and inject a loader, or build an [edge app](/docs/developers/edge-apps/) instead.

Edge apps do not use this field. An app ships its own `screenly_inject.js` alongside its manifest, and the player runs that instead.

## Worked examples

The [Screenly Playground](https://github.com/Screenly/Playground/tree/master/javascript-injectors) holds working injectors for common cases. Test a script in a normal browser console against the same page before you attach it, because debugging one on a wall is slow.