---
title: Libraries
description: >-
  Share functions and types across your scripts: create a library in the
  editor, import it with @owner/name, pin versions, and publish when it is
  ready. Library code is frozen into your scripts at save, so charts never
  depend on a registry at runtime.
verified:
  engine: 3.0.67
---

<div class="flex gap-3 mb-6">
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-purple-50 text-purple-600 text-sm font-medium">
    Advanced
  </span>
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 text-sm font-medium">
    10 min read
  </span>
</div>

## Introduction

Libraries let you write shared functions and types **once** and import them from any script: your normalization math, your risk model, your zone structs. A library is itself kScript, runs under exactly the same sandbox as the script importing it, and while you edit a library its open draft resolves live in your editor, so a script tab and a library tab iterate side by side.

Libraries live in the **Libraries panel** of the script editor. Creating one opens it as a regular editor tab with the same highlighting and diagnostics as a script.

## Writing a library

A library source starts with a `library()` header, followed by **only** function and type declarations:

```javascript
library("norm")

func zscore(sample, mean, dev) {
  if (dev == 0) { return 0 }
  return (sample - mean) / dev
}

// +1 stretched up, -1 stretched down, 0 neutral.
func regime(z, hi, lo) {
  if (z > hi) { return 1 }
  if (z < lo) { return -1 }
  return 0
}
```

- The name is lowercase (`[a-z][a-z0-9_]*`). Anything else at top level (`define`, plots, `input`, data sources, bare expressions) is a compile error: libraries declare, consumers run.
- **Function arguments are evaluated per bar.** A library function receives samples (`closes[0]`), structs, and plain values, and returns per-bar results. It cannot receive a whole timeseries, so TA calls like `sma(...)` belong in the consumer script, which passes the current values in. Libraries hold the math, the thresholds, the types, and the decision logic.

## Importing

The conventional script layout is the version marker first, imports next, then `define()`:

```javascript
// @version=3
import "@you/norm"                  // latest published version, plain names: zscore(...)
import "@you/norm@1.0.0"            // pinned to an exact version
import "@you/norm" as norm          // aliased: norm.zscore(...), norm.Band.new(...)
define("My Indicator", "offchart", true)
```

The compiler accepts these lines in any order, but keep the marker on line one and imports above `define()` so every script reads the same way.

- Libraries are addressed by their **owner-scoped name** (`@owner/name`), which is what the Libraries panel shows and what import autocomplete inserts. The `library()` header inside the source keeps the bare name.
- **Plain import** brings the library's functions and types in under their own names. **Alias import** namespaces everything behind the alias. **Pinning** (`@owner/name@1.0.0`) selects an exact published version; a bare name takes the latest.
- One placement rule: inside a `timeseries` initializer, call plain-imported functions (`zscore(...)`). Alias-namespaced calls (`norm.zscore(...)`) are not resolvable in series expressions; they work everywhere else (statements, conditions, plot arguments).

## Using what you imported

Consumer scripts own the data and the TA; the library owns the shared logic:

```javascript
// @version=3
import "@you/norm"
define("Momentum Z", "offchart", true)

timeseries d = ohlcv(currentSymbol, currentExchange)
timeseries closes = d.close
timeseries mean = sma(source=closes, period=20)
timeseries dev = stdev(source=closes, period=20)

timeseries z = zscore(closes[0], mean[0], dev[0])
plotLine(z, colors=["#4f8cff"], label=["Z-Score"], desc=["Z-score of close"])
plotLine(regime(z[0], 2, -2), colors=["#f59e0b"], label=["Regime"], desc=["+1 stretched up, -1 down"])
```

Change a threshold inside the library, publish, and every script that re-saves picks it up: the fix lands once instead of being copy-pasted across five indicators.

## Drafts, saving, and publishing

A library has two states, and the split is what keeps shipped scripts safe:

- **Draft** is your private scratch state. Save (`Cmd+S`) persists it, and while a library tab is open, your script tabs lint and run against the live draft text on every keystroke.
- **Publish** turns the draft into an immutable version (`1.0.0`, with notes). Published versions never change; publishing the same version number again is an error.

When you save or publish a **script**, the published library code it imports is frozen into the script itself. Charts, server runs, and alerts never look libraries up at runtime, so a shipped script cannot slow down or break because a registry is slow or unavailable.

Because only published versions can be frozen, saving a script that imports a library with no published versions is rejected: the save banner names the import and ends with "Publish the library, then save again." Publish first, then save the script.

## Versions and staleness

- An unpinned import (`@you/norm`) tracks the **latest published version at each script save**. When the library publishes something newer, your script shows a staleness notice and adopts the new version the next time you save it.
- A pinned import (`@you/norm@1.0.0`) never moves. Pin in strategies you have validated: the backtest you trusted keeps computing exactly the same way even after the library evolves. Pinned imports never show the staleness notice; instead the import line carries a quiet dashed underline when newer versions exist, and hovering it shows which one. It informs, and changes nothing.

```javascript
library("sessions", "1.2.0")

// UTC day bucket of a millisecond timestamp.
func day_bucket(ts) {
  return floor(ts / 86400000)
}

func is_new_day(ts, prevTs) {
  return day_bucket(ts) != day_bucket(prevTs)
}
```

```javascript
// @version=3
import "@you/sessions@1.2.0" as ses
define("Daily Open", "onchart", false)

timeseries d = ohlcv(currentSymbol, currentExchange)
persist dayOpen = 0
if (ses.is_new_day(d.time[0], d.time[1])) {
  dayOpen = d.open[0]
}
plotLine(dayOpen, colors=["#f59e0b"], label=["Daily Open"], desc=["Open of the current UTC day"])
```

## Sharing

Every library has a visibility toggle in the Libraries panel. **Private** libraries are importable only by you. **Public** libraries can be imported by anyone who knows the exact `@owner/name`. There is no browse or marketplace surface yet; share the name directly.

## Managing libraries

Right-click a library in the panel for its actions: open in the editor, copy the import line, flip visibility, and delete. **Deleting is draft-only**: a library with no published versions belongs to nobody but you and can go (its editor tab closes and imports of it resolve as unknown again). Once versions are published they are immutable dependency targets, so the library can no longer be deleted; withdrawing a published version is a planned separate flow.

## Worked example: a shared type

Types travel across the import boundary too, so two scripts can agree on what a "zone" is instead of each declaring its own struct:

```javascript
library("zones")

type Zone {
  top: number,
  bottom: number,
  touches: number
}

func zone_from_bar(high, low) {
  return Zone.new(top=high, bottom=low, touches=0)
}

func zone_contains(zone, price) {
  return price <= zone.top && price >= zone.bottom
}
```

```javascript
// @version=3
import "@you/zones" as zones
define("Zone Touches", "offchart", true)

timeseries d = ohlcv(currentSymbol, currentExchange)
static z = zones.zone_from_bar(0, 0)
if (d.high[0] > z.top) {
  z = zones.zone_from_bar(d.high[0], d.low[0])
}
if (zones.zone_contains(z, d.close[0])) {
  z.touches = z.touches + 1
}
plotLine(z.touches, colors=["#4f8cff"], label=["Touches"], desc=["Re-entries into the active zone"])
```

## Precedence and integrity rules

Worked out so that nothing ever changes meaning silently:

1. **Your script wins.** If a plain import collides with a function the script itself defines, the script's version is used at the script's call sites, and the compiler emits a warning telling you so.
2. **Libraries are internally sealed.** A library function calling its sibling always gets *its own* sibling, even when the consumer overrides that name.
3. **Import-vs-import plain collisions are errors** (ambiguity has no safe default); alias one of them.
4. **Aliases are validated**: `as math` (a builtin namespace) or `as close` (a global) is a compile error.

## Sandbox and attribution

Library code is ordinary kScript: every limit in the [sandbox manifest](../faq/limitations.md) applies unchanged, and errors raised inside a library carry the **library's own line and column**, so a runaway loop in `@you/norm` says so, not some opaque consumer position.

## Troubleshooting

What the editor tells you when an import is wrong, and how to fix it:

| Situation | Diagnostic you will see | Fix |
| --- | --- | --- |
| Library name does not exist | `Unknown library '@you/nope'` on the import line | Check the name in the Libraries panel; autocomplete inserts the exact form |
| Pinned version does not exist | `Unknown library '@you/norm@2.0.0'; available versions: 1.0.0` | Pin one of the listed versions, or drop the pin for latest |
| Malformed name or version pin | `Invalid library name in import. Names are lowercase letters, digits, and underscores, starting with a letter, with an optional @x.y.z version pin. Examples: import "my_lib", import "@owner/my_lib", import "@owner/my_lib@1.2.0"` on the import line | Match one of the three example shapes in the message; the note below the table lists every form that trips it |
| Alias shadows a builtin or global | `alias collides with builtin` | Pick another alias |
| Two plain imports export the same name | `Imported function 'zscore' from library '@b/other' collides with imported declaration from library '@you/norm'` | Alias one of the imports |
| Calling something the library does not export | `Undefined function 'not_in_lib'`, with did-you-mean suggestions | Check the library tab for the exported names |
| The library itself has an error | `Library @you/norm failed to compile: <its first error>` on your script, and the precise line inside the library tab | Fix the library; your script re-lints as you type |
| Aliased call inside a `timeseries` initializer | Lints clean but **fails on Run** with `Undefined identifier 'norm'` | Use a plain import for functions you call in series expressions |
| Saving a script that imports an unpublished library | Save banner: names the import, ends with "Publish the library, then save again." | Publish the library, then save the script |

Every malformed import gets that same one "Invalid library name" diagnostic. It fires on an uppercase letter in the name segment (`import "MyLib"`, `import "@you/Norm"`), a name starting with a digit (`import "2fast"`), a missing name after the owner (`import "@you"`, `import "@you/"`), and a version pin that is not exactly three numeric segments (`import "@you/norm@1.0"` and `import "norm@v1.0.0"` are both rejected: no two-segment shorthand, no `v` prefix). Two edges are easy to miss. The lowercase rule binds only the name segment; the owner segment also accepts uppercase letters, digits, dots, and dashes, so `import "@Kyle.T-77/norm"` is valid. And all three shapes in the message's `Examples:` list really import: the bare form `import "norm"` is accepted and resolves by the library's `library("norm")` header name.

## Pine comparison

Pine libraries are publish-bound: TradingView-hosted, review-queued, export only functions over Pine's types. kScript libraries iterate live in your editor as drafts, publish instantly under your own name, pin by version, export your structured types, and their consumers keep every compile-time guarantee (typed fields, precedence warnings, sandbox attribution) the rest of v3 provides.
