Skip to content

api.timeline

The map's Timeline from an action: the running timed effects, and starting, changing and ending one.

Part of the Action API · Function reference · Since Action API 0.6 (v0.104.0)

Signature

api.timeline.list(): Promise<TimedEffect[]>
api.timeline.createEffect(input: EffectInput): Promise<TimedEffect>
api.timeline.editEffect(id: string, patch: Partial<EffectInput>): Promise<TimedEffect>
api.timeline.endEffect(id: string): Promise<TimedEffect>
api.timeline.startBuff(buff: Buff, opts?: { tokenId?, url?, color?, parent? }): Promise<TimedEffect>
api.timeline.endEffects(filter: { tokenId?: string; category: string | string[] }): Promise<{ ended, refused }>   // 0.8

Asynchronous — await each.

interface EffectInput {
  name: string;
  duration: number | { fn: string };   // rounds (0 or more), -1 for indefinite, or a function
  description?: string;
  url?: string;                         // defaults to this action's document (the power it came from)
  color?: string;                       // '#rrggbb'; defaults to the next distinct hue on the map
  startTick?: number;                   // defaults to now
  tokenId?: string;                     // the token the effect is on
  positivity?: 'positive' | 'neutral' | 'negative';
  effects?: BuffEffect[];               // 0.8: what it does to the bearer's numbers while it runs
  category?: string;                    // 0.8: Magic, Curse, Poison… — what cleansing ends
  links?: { type: 'drawing' | 'token' | 'effect'; id: string }[];   // 0.8: 'effect' — ends when that one does
}

Durations

A number is rounds: the effect ends at the same place in initiative that many rounds later (10 — ten rounds; 0 — the end of this turn's round position; -1 — indefinite, until ended by hand). A function decides at every tick:

duration: { fn: '(round, whoseTurn) => round >= 5' }

It runs in the platform's script sandbox — a small allowlisted language, not full JavaScript — with round (the current round) and whoseTurn (the token acting: { id, name, initiative, requestRoll }), and ends the effect when it returns true. whoseTurn.requestRoll('1d20+2') asks whoever plays that token to roll; the platform pauses the function until the roll is answered and then runs it again with the result in hand — { total }, or null if they declined — so write it as a plain call, not an await:

duration: { fn: '(round, whoseTurn) => { if (whoseTurn.id !== "TOKEN_ID") return false; const r = whoseTurn.requestRoll("1d20+3", { label: "Wisdom save" }); return r !== null && r.total >= 15; }' }

A function that throws leaves the effect running and tells whoever started it what went wrong, once.

Buffs

api.timeline.startBuff(buff, { tokenId?, url?, color?, parent? }) starts a buff on the Timeline. Its duration becomes the effect's:

  • a timed buff counts rounds by its unit (a minute is ten rounds, an hour six hundred);
  • concentration and save ends become functions that decide (the platform supplies the shape; the action may write its own);
  • a buff whose duration already names an effect (type: 'effect', effectId) returns that effect.

The slice carries the buff's name and description.

Since Action API 0.8 (v0.117.0) a buff does something. Its effects change the bearer's numbers while the effect runs. Its positivity colours the bearer's Mini-Me chip, and its category is what cleansing ends.

{
  name: 'Shield of Faith',
  category: 'Magic',
  positivity: 'positive',
  duration: { type: 'concentration' },
  effects: [{ kind: 'attribute', target: 'ac', op: 'add', value: 2 }],
}

An attribute effect has a target from the system's vocabulary (ac, ac.base, ability.str, speed.walk, save.*…) and one of five operations. They are applied in stages, so the order they arrive in never matters:

op Does Several resolve by
set replaces the base (Mage Armor: ac.base = '13 + @dex.mod') the highest wins
add adds (Shield of Faith: ac +2) all apply
percent adds a percent of the base (Haste: speed.* +100) percents sum, then apply once
floor at least (Barkskin: ac 16) the highest wins
ceiling at most (Restrained: speed.* 0), applied last the lowest wins

A condition effect, { kind: 'condition', conditionId: 'paralyzed' }, puts that condition on the bearer for as long as the buff runs.

The system decides stacking. In 5e, two castings of the same spell do not stack: the most potent applies.

parent (an effect, or its id) ties the buff to another effect, and it ends when that one does. A concentration spell passes the caster's concentration effect, so dropping concentration ends the buff on every target:

const conc = await api.timeline.concentrate({ name: 'Bless', duration: 10 });
for (const t of targets) await api.timeline.startBuff(bless, { tokenId: t.id, parent: conc });

Cleansing

api.timeline.endEffects({ tokenId?, category }) ends every running effect on a token whose category matches. The token defaults to the acting one. category is a string or a list, matched case-insensitively:

await api.timeline.endEffects({ tokenId: target.id, category: 'Magic' });              // Dispel Magic
await api.timeline.endEffects({ tokenId: target.id, category: ['Poison', 'Disease'] }); // Lesser Restoration

Each end is the server's to allow, as with endEffect. An effect the server refuses is listed in refused with its reason, and the call does not throw.

What a derived power does (v0.104.2)

A System 8 power action the platform derives from its document writes this call itself when the document has a duration: one createEffect per token in targets (or target), or on token when the range is personal and nothing was aimed at; duration is the rounds the document counts, -1 for concentration / encounter / sustained / permanent, and for a save that ends (Str ends) a function built at run time from the bearer's id and the power roll — the save DC in System 8:

// What Entangle (circle 2, concentration, Str ends) sends for a target whose roll to beat was 14.
{ fn: '(round, whoseTurn) => { if (!whoseTurn || whoseTurn.id !== "gnoll-1") return false; const r = whoseTurn.requestRoll("1d20+str"); return r !== null && r.total >= 14; }' }

The first effect takes the palette's colour and the rest reuse it, so one casting is one hue. See The Timeline.

Example

// Bless, ten rounds, on the target; the slice links back to the spell.
const bless = await api.timeline.createEffect({ name: 'Bless', duration: 10, tokenId: target.id });

// Dispel Magic: end the first magical effect on the target.
const running = await api.timeline.list();
const onTarget = running.find((e) => e.tokenId === target.id);
if (onTarget) await api.timeline.endEffect(onTarget.id);

Permissions, and a map

The server's rules apply as they do to a hand on the Timeline strip: anyone who may play on the map starts an effect; the one who started it, or the GM, changes or ends it. Each call needs an open map — the impersonated-token bar and the map are where actions run — and throws with a plain sentence without one.