Amba

Streaks

Track consecutive user engagement with configurable streak mechanics, grace periods, and freezes.

Streaks track consecutive user engagement over time. Define a qualifying event (like app_open or workout_completed), and Amba automatically tracks current count, longest count, and streak status.

How it works

  1. Admin defines a streak definition with a qualifying event and period
  2. When a user tracks the qualifying event, their streak is automatically updated
  3. If the user misses a period, the streak resets (unless a grace period or freeze is configured)
  4. Streak evaluation also runs daily in the background to catch broken streaks

Worked example: define once, qualify per user action

The full loop is two steps. You define the streak once (admin / MCP), then your app calls qualify per qualifying user action.

Snippets below import from @layers/amba-web for brevity. The wrapped Amba.streaks API is identical across @layers/amba-web, @layers/amba-node, @layers/amba-react-native, and @layers/amba-expo — substitute the package that matches your platform.

1. Define a streak (one-time, via MCP — usually from your dev shell or admin tool):

// Via the Amba MCP server (mcp.amba.dev/mcp). Use any MCP-aware agent
// (Claude Code, Cursor, Windsurf) or call the tool directly.
await amba_create_streak({
  project_id: 'prj_…',
  key: 'daily_play', // stable key your app calls qualify with
  name: 'Daily Player',
  period: 'daily', // 'daily' | 'weekly'
  qualifying_event: 'session_started',
  grace_period_hours: 6, // optional — extra time before a miss
  freeze_enabled: true,
  max_freezes: 2,
  freezes_per_n_events: 5, // auto-grant 1 shield every 5 qualifying events
});

2. Qualify the streak from your app on every user action:

import { Amba } from '@layers/amba-web';
 
// On every "user did the thing" event (e.g. finished a daily session)
const streak = await Amba.streaks.qualify('daily_play');
 
// streak.current_count     = 7        ← they're on a 7-day streak
// streak.longest_count     = 12       ← personal best
// streak.status             = 'active'
// streak.freezes_remaining  = 1        ← unused shields in reserve

3. Handle the "streak isn't defined yet" case cleanly:

try {
  await Amba.streaks.qualify('weekly_login');
} catch (e) {
  if (e?.error?.code === 'STREAK_NOT_FOUND') {
    // The streak hasn't been defined for this project yet — call
    // amba_create_streak first. This is a config-time error, never
    // a runtime one; should not reach production.
    console.error('Define this streak using amba_create_streak first.');
  } else {
    throw e;
  }
}

SDK usage

Get user's streaks

const streaks = await client.streaks.getAll();
// Returns all streak states with definitions

Each Streak carries:

interface Streak {
  id: string;
  /** Stable key — use this with `qualify()`. e.g. `"daily_play"` */
  key: string;
  name: string; // e.g. "Daily Workout"
  current_count: number; // current streak count
  longest_count: number; // all-time best
  last_qualified_at?: string | null; // ISO timestamp of the last qualifying event
  /**
   * Lifecycle state. One of:
   *   "active"  — streak is alive
   *   "broken"  — missed a period; current_count reset to 0
   *   "frozen"  — a shield (freeze) protected the streak from a miss
   */
  status: 'active' | 'broken' | 'frozen';
  /**
   * Unused streak-shield count. Auto-granted by the server every
   * `freezes_per_n_events` qualifying events, capped at `max_freezes` (both
   * configured on the streak). Drops by 1 when a shield is consumed on a miss.
   */
  freezes_remaining: number;
  updated_at: string;
}

Use status to drive UI copy ("You're on a 7-day streak!" vs. "Your streak is safe — we used a shield"), and freezes_remaining to show the user how many shields they have in reserve.

Qualify a streak manually

// Usually automatic via event tracking, but you can also qualify directly:
await client.streaks.qualify('daily_play');

Automatic qualification

When you track an event that matches a streak's qualifying_event, the streak is automatically qualified:

// This both tracks the event AND qualifies the "Daily Workout" streak
await Amba.events.track('workout_completed', { duration: 30 });

Streak mechanics

Period

  • daily — user must qualify once per calendar day
  • weekly — user must qualify once per calendar week

Grace period

Set grace_period_hours to give users extra time before their streak breaks. For example, a 6-hour grace period means the user has until 6 AM the next day.

Streak freezes

When freeze_enabled is true, users can freeze their streak to prevent it from breaking. max_freezes controls how many freezes a user can hold at once.

Shields (freezes)

In partner-facing copy we call freezes "shields" — same field on the wire (freezes_remaining), different word for the user. A streak with 1 shield that misses its qualifying period is automatically converted from active to frozen instead of broken, and freezes_remaining is decremented by 1. The streak's current_count is preserved — never punish a missed day with a red number; show "your streak is safe" copy instead.

Field semantics

Streak configuration (set when creating a streak via amba_streaks_create):

FieldTypeMeaning
freeze_enabledbooleanMaster switch. If false, missing the period always breaks the streak.
max_freezesintegerCap on how many shields a user can hold. Both auto-grant and admin grant clamp at this value.
freezes_per_n_eventsinteger | nullAuto-grant rule: every N consecutive qualifying events grants 1 shield. null disables auto-grant — shields only come from explicit admin grants.

Per-user streak state (returned by the client SDK):

FieldTypeMeaning
freezes_remainingintegerNumber of shields currently in the user's pool. Drops by 1 when a shield is consumed on a miss.
statusenum'active' | 'frozen' | 'broken'. frozen is the moment a shield protected the streak.

Auto-grant rule

Set freezes_per_n_events = 5, max_freezes = 2 to get the partner spec verbatim: every 5 completed missions grants the user 1 shield, capped at 2 concurrent shields.

await admin.streaks.create({
  key: 'daily_workout',
  name: 'Daily Workout',
  qualifying_event: 'workout_completed',
  period: 'daily',
  freeze_enabled: true,
  max_freezes: 2,
  freezes_per_n_events: 5,
});

The grant fires on the increment that crosses the threshold (i.e. when current_count % freezes_per_n_events === 0). Resets and restarts do NOT grant a shield.

Shield consumption emits streak_shielded

When the daily streak-evaluation finds a streak that missed its window and the user still has at least one shield, the streak flips to frozen, freezes_remaining decrements by 1, and a streak_shielded engagement event is emitted:

{
  "event_name": "streak_shielded",
  "properties": {
    "streak_id": "us_…",
    "streak_definition_id": "sd_…",
    "freezes_remaining_after": 0
  }
}

Use this event to render in-app "your streak is safe" copy or to drive a re-engagement push.

Granting shields manually

Grant additional shields to a specific user (admin-only) via the streak admin tooling:

POST /v1/admin/projects/:projectId/users/:userId/streaks/:streakDefinitionId/grant-shield
Body: { count?: number }   // default 1

Grants the user count additional shields, clamped at the streak's max_freezes. Returns the updated streak state. 404 if the user hasn't qualified for this streak yet — they must trigger the qualifying event at least once before a grant can land.

Admin API reference

MethodPathDescription
POST/admin/streaksCreate streak definition
GET/admin/streaksList streak definitions

Client API reference

MethodPathDescription
GET/client/streaksGet user's streak states
POST/client/streaks/:id/qualifyRecord qualifying event

POST /client/streaks/:id/qualify

Records a qualifying event for a streak. Handles:

  • Already qualified today (returns current state, no-op)
  • Consecutive day (increments count)
  • Broken streak (resets to 1)

Response:

{
  "data": {
    "id": "uuid",
    "current_count": 8,
    "longest_count": 14,
    "last_qualified_at": "2025-01-15T10:30:00Z",
    "status": "active",
    "current_period_start": "2025-01-08"
  }
}

MCP tools

ToolDescription
amba_create_streakCreate a streak definition with qualifying event, period, grace, and freeze settings

Example

Agent: "Create a daily login streak with a 6-hour grace period"

amba_create_streak({
  project_id: "proj_xxx",
  key: "daily_login",
  name: "Daily Login",
  qualifying_event: "app_open",
  period: "daily",
  grace_period_hours: 6,
  freeze_enabled: true,
  max_freezes: 2
})

Reading streaks in React Native / Expo

The component below is React Native — runs identically on bare RN and Expo. Swap the import to match your SDK package: @layers/amba-react-native for bare RN, @layers/amba-expo for Expo managed.

import { useEffect, useState } from 'react';
// Bare React Native:
//   import { Amba, type Streak } from '@layers/amba-react-native';
// Expo managed:
import { Amba, type Streak } from '@layers/amba-expo';
 
function StreakCard() {
  const [streaks, setStreaks] = useState<Streak[]>([]);
 
  useEffect(() => {
    Amba.streaks.getAll().then(setStreaks);
  }, []);
 
  const daily = streaks.find((s) => s.key === 'daily_login');
  if (!daily) return null;
 
  const safeCopy = daily.status === 'frozen' ? 'Your streak is safe — a shield kicked in.' : null;
 
  return (
    <View>
      <Text>Current streak: {daily.current_count} days</Text>
      <Text>Longest streak: {daily.longest_count} days</Text>
      <Text>Shields in reserve: {daily.freezes_remaining}</Text>
      {safeCopy ? <Text>{safeCopy}</Text> : null}
    </View>
  );
}