---
title: "String functions"
description: "Strings in an Indicator live in string slots: byte-capped channels written once per ready bar in finalize() and read by text renderers and drawing labels. They…"
order: 50
section: "functions"
---

<!-- source: docs/indicators/functions/string-functions.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# String functions

Strings in an Indicator live in string slots: byte-capped channels written
once per ready bar in `finalize()` and read by text renderers and drawing
labels. They never enter the module (params are numbers, inputs are
numbers) and they never become metrics. On the way in, AssemblyScript's
`String` has the JavaScript-style methods kScript (legacy) offered; on the
way out, the generated `sb_*` builder formats numbers into a slot without
allocating. This page is both halves and the one gotcha the declaration
name causes.

## Where strings go

Declare a slot, then send text to it from `finalize()`:

```typescript
string("note", { max_bytes: 64 });
render.text("note_mark", { y: "average", text: "note" });
```

The generated `./gen/strings` module gives you, per slot, `str_note(s)`
(encode and send a whole string) and `str_note_sb()` (send the shared
line buffer), plus the builder that fills that buffer:

| Builder | Appends |
| --- | --- |
| `sb_clear()` | nothing: starts a new line (call it first) |
| `sb_text(s)` | a string's UTF-8 |
| `sb_int(n)` | an integer (`i64`) |
| `sb_f64(x, decimals)` | a number in fixed point with `decimals` (0..9) fraction digits, rounded half up; `NaN`, `Inf`, `-Inf` spelled out |

A line over the slot's `max_bytes` is refused host-side by name (strings
are never truncated); a slot not written that bar is ABSENT, which is
distinct from a written empty string, and the difference is what gates
`render.text` (an absent slot draws nothing). The senders trap by phase
outside `finalize()`.

## The `string` name

The declaration is called `string`, the same word as the type. In a file
that imports it, `let label: string = "";` fails to compile (`string` is
now a function). Two ways out: let the type be inferred (`let label =
"";`), or alias the import, which the extractor still recognizes:

```typescript
import { string as slot } from "./sdk/declare";
slot("note", { max_bytes: 64 });
let label: string = "";
```

## Available methods

AssemblyScript's `String` matches the kScript table method for method.
They work on `string` values in the module, in `init()` or per bar, and
the result goes out through `str_<slot>(s)`.

| Method | Signature | Description |
| --- | --- | --- |
| `split` | `s.split(separator)` | splits into an array of substrings (`string[]`) |
| `concat` | `s.concat(other)` | joins two strings; `+` does the same |
| `substring` | `s.substring(start, end?)` | the characters between two indexes |
| `toUpperCase` | `s.toUpperCase()` | uppercase copy |
| `toLowerCase` | `s.toLowerCase()` | lowercase copy |
| `trim` | `s.trim()` | whitespace removed from both ends (`trimStart`, `trimEnd` too) |
| `replace` | `s.replace(search, replaceWith)` | the first occurrence replaced (`replaceAll` for every one) |
| `indexOf` | `s.indexOf(search)` | the index of the first occurrence, `-1` if absent |
| `startsWith`, `endsWith` | `s.startsWith(prefix)` | prefix and suffix tests |
| `includes` | `s.includes(search)` | containment test |
| `length` | `s.length` | the number of UTF-16 units; a property, not a call |
| `charCodeAt` | `s.charCodeAt(i)` | the code unit at `i` |
| `padStart`, `padEnd` | `s.padStart(width, fill)` | padding to a width |

Numbers do not stringify themselves cheaply: `sb_f64` and `sb_int` are
the way to put a number in a line, and the builder is where per-bar text
belongs.

## Allocation

Every `String` method that returns a new string allocates it, and the
module's runtime never frees: the sandbox compiles with a bump allocator
and a 4 MiB memory ceiling, and a module that grows memory after `init()`
is refused. Per-bar `String` work therefore accumulates for the whole
history. The `sb_*` builder is allocation-free by design (one shared
buffer sized to the largest slot at module start), so the rule is:
`String` methods for one-time work in `init()` or at module start (a
label, a market name, a template), `sb_*` for everything that happens
per bar.

## Every method in one module

A label assembled once in `init()` with the `String` methods, and per-bar
text built with the `sb_*` builder into a mark and a live tag.

```typescript
import { input, line, none, ohlcv, output, overlay, param, render, string as slot, time } from "./sdk/declare";
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_average, out_bar_time, out_stretched } from "./gen/outputs";
import { p_period } from "./gen/params";
import { sb_clear, sb_f64, sb_int, sb_text, str_readout_sb, str_tag_sb } from "./gen/strings";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 1, max: 200 });
input("close", ohlcv.close);
input("bar_t", time.bar_open_sec);
output("average", line, overlay, { color: "#38bdf8", width: 2, description: "Simple average" });
output("bar_time", none, overlay, { description: "Bar open in epoch seconds" });
output("stretched", none, overlay, { description: "1 while the close is over two percent from the average" });
slot("readout", { max_bytes: 48 });
slot("tag", { max_bytes: 48 });
render.text("stretch_mark", { y: "average", text: "readout", size: 10 });
render.label("average_tag", { x: "bar_time", y: "average", text: "tag", size: 11 });

let sma = new Sma(20);
let label: string = "";
let period: i32 = 20;
let value: f64 = NaN;
let close: f64 = NaN;
let barTime: f64 = NaN;
let barIndex: i32 = 0;

export function init(): void {
  period = i32(p_period());
  sma = new Sma(period);
  // One-time string work: every method from the table, on a market label.
  const raw = "  btc-usdt:perp  ";
  const parts = raw.trim().split(":");
  let name = parts[0].toUpperCase().replace("-", "/");
  if (name.startsWith("BTC") && name.endsWith("USDT") && name.indexOf("/") == 3 && name.includes("USD")) {
    name = name.concat(" ").concat(parts[1].toLowerCase());
  }
  label = name.substring(0, name.length).padEnd(12, ".");
}

export function state(): i32 {
  close = in_close();
  barTime = in_bar_t();
  value = sma.update(close);
  barIndex += 1;
  return isNaN(value) ? 0 : 1;
}

export function finalize(): void {
  const stretch = ((close - value) / value) * 100.0;
  const stretched = Math.abs(stretch) >= 2.0;
  out_average(value);
  out_bar_time(barTime);
  out_stretched(stretched ? 1.0 : 0.0);
  if (stretched) {
    // Per-bar text: the builder, no allocation.
    sb_clear();
    sb_f64(stretch, 1);
    sb_text("% at bar ");
    sb_int(barIndex);
    str_readout_sb();
  }
  sb_clear();
  sb_text(label);
  sb_text(" SMA ");
  sb_f64(value, 2);
  str_tag_sb();
  emitRow();
}

export function reset(): void {
  sma.reset();
  value = NaN;
  close = NaN;
  barTime = NaN;
  barIndex = 0;
}
```

`label` is built once and reused on every bar; `reset()` leaves it alone
because `init()` owns it. The readout is written only on stretched bars,
so the mark appears only there.

## Caps

Per slot `max_bytes` is at most 4096; 64 slots per package; 64 KiB of
string bytes per row; 8 MiB per run; each a named refusal
([Limits](../reference/limits.md)).
