Build on Malini

Events

Every event Malini emits, when it fires, and the TypeScript shape of its payload.

Open as markdown

Malini announces everything that happens on the platform as an event on a <domain>:<event> channel. Routines can trigger on any of them, and agents inside Malini can list them with the events_list tool. This page is generated from the contract in apps/malini/src/contract/events.ts, so it always matches the app.

chat:agent-event

Fires for every event an agent run streams in a chat, such as text, tool calls, approvals and the end of the run, in sequence order.

Payload: LiveEventEnvelope

interface AgentEventEnvelope {
	sessionId: string;
	runId: string;
	seq: number;
	event: Record<string, unknown>;
}

type LiveEventEnvelope = AgentEventEnvelope & { ephemeral?: true };

chat:run-changes-captured

Fires after an agent run that changed files finishes and Malini has recorded the diff of that run.

Payload: RunChangesCapturedPayload

interface RunChangesCapturedPayload {
	sessionId: string;
	runId: string;
}

chat:checkpoint-restored

Fires after a workstream's files are restored to a chat checkpoint, rewinding the changes made after it.

Payload: CheckpointRestoredPayload

interface CheckpointRestoredPayload {
	workstreamId: string;
	checkpointId: string;
	sessionId: string;
}

chat:session-renamed

Fires when a chat gets a new name, from the user or from the Name chats job.

Payload: SessionRenamedPayload

interface SessionRenamedPayload {
	workstreamId: string;
	sessionId: string;
	name: string;
}

repositories:workstream-created

Fires when a new workstream exists: its git worktree is checked out and it is ready for chats.

Payload: WorktreeAddedPayload

interface GitProjectRecord {
	id: string;
	name: string;
	repoPath: string;
	defaultBranch: string;
	createdAt: string;
}

interface GitWorkstreamRecord {
	id: string;
	projectId: string;
	name: string;
	path: string;
	branch: string;
	baseBranch: string;
	status: string;
	createdAt: string;
}

interface WorktreeAddedPayload {
	workstreamId: string;
	project: GitProjectRecord;
	workstream: GitWorkstreamRecord;
}

repositories:workstream-removed

Fires when a workstream leaves the sidebar, with archived set when it was archived and deleted set when it was deleted.

Payload: WorktreeRemovedPayload

interface WorktreeRemovedPayload {
	workstreamId: string;
	worktreePath: string;
	archived?: true;
	deleted?: true;
}

repositories:workstream-files-changed

Fires when files change in a workstream's checkout, with the changed paths, or null paths when Malini could not tell which files changed.

Payload: WorkstreamFilesChangedPayload

interface WorkstreamFilesChangedPayload {
	workstreamId: string;
	changedAt: string;
	paths: string[] | null;
}

repositories:workstream-renamed

Fires when a workstream gets a new name.

Payload: WorkstreamRenamedPayload

interface WorkstreamRenamedPayload {
	workstreamId: string;
	name: string;
}

repositories:workstream-install-status

Fires as a new workstream's dependency install moves through its states: running, succeeded, failed, skipped or aborted.

Payload: WorkstreamInstallStatusPayload

type InstallStatus = 'running' | 'succeeded' | 'failed' | 'skipped' | 'aborted';

type InstallSkipReason = 'already-installed' | 'no-manifest' | 'unrecognized-project';

interface WorkstreamInstallStatusPayload {
	type: 'repositories:workstream-install-status';
	workstreamId: string;
	status: InstallStatus;
	command?: string;
	reason?: InstallSkipReason;
	detail?: string;
	durationMs?: number;
	exitCode?: number;
}

repositories:clone-progress

Fires repeatedly while Malini clones a repository, with the stage and the fraction done, and once more when it is done.

Payload: CloneProgressPayload

interface CloneProgressPayload {
	repo_id: string;
	stage: 'fetch' | 'checkout' | 'done';
	fraction: number;
}

pull-requests:local-base-synced

Fires after Malini merges a pull request and tries to bring the repository's local base branch up to date, with the outcome.

Payload: LocalBaseSyncedPayload

interface LocalBaseSyncedPayload {
	workstreamId: string | null;
	checkout: string;
	branch: string;
	outcome: 'advanced' | 'diverged' | 'failed';
	detail?: string;
}

pull-requests:ship-changed

Fires whenever a workstream's shipping state changes: its pull request phase, the merge queue step, or a reason it needs you.

Payload: ShipChangedEvent

type ShipPhase = 'local' | 'draft' | 'queued' | 'needs-you';

type ShipStep =
	| 'waiting-for-agent'
	| 'opening-draft'
	| 'pushing'
	| 'marking-ready'
	| 'updating-branch'
	| 'updating-from-main'
	| 'fixing-checks'
	| 'addressing-review'
	| 'waiting-for-checks'
	| 'waiting-for-review'
	| 'waiting-for-github'
	| 'merging'
	| 'archiving';

type ShipNeedsYouReason =
	| 'reply-to-reviewer'
	| 'fix-gave-up'
	| 'merge-not-permitted'
	| 'pull-request-closed'
	| 'github-reconnect'
	| 'operation-in-progress';

type ShipNeedsYou = Readonly<{
	reason: ShipNeedsYouReason;
	detail: string;
	jobRunId: string | null;
}>;

type ShipStateRecord = Readonly<{
	workstreamId: string;
	phase: ShipPhase;
	step: ShipStep | null;
	pullRequestNumber: number | null;
	pullRequestUrl: string | null;
	needsYou: ShipNeedsYou | null;
	queuedAt: string | null;
	attempts: number;
	activeJobRunId: string | null;
	lastError: string | null;
	nextAttemptAt: string | null;
	updatedAt: string;
}>;

type ShipMerged = Readonly<{ pullRequestNumber: number; pullRequestUrl: string | null }>;

type ShipChangedEvent = Readonly<{
	workstreamId: string;
	state: ShipStateRecord | null;
	merged?: ShipMerged;
}>;

jobs:run-changed

Fires whenever a job run is queued, starts, needs you, or finishes as succeeded, failed or cancelled.

Payload: JobRunChangedEvent

type JobRunOrigin = 'merge-queue' | 'manual' | 'routine';

type JobRunStatus = 'queued' | 'running' | 'needs-you' | 'succeeded' | 'failed' | 'cancelled';

type JobRunRecord = Readonly<{
	id: string;
	definitionId: string;
	title: string;
	workstreamId: string | null;
	repositoryId: string | null;
	sessionId: string | null;
	agentRunId: string | null;
	origin: JobRunOrigin;
	status: JobRunStatus;
	detail: string | null;
	chatSessionId: string | null;
	chatAfterRunId: string | null;
	createdAt: string;
	startedAt: string | null;
	finishedAt: string | null;
}>;

type JobRunChangedEvent = Readonly<{ run: JobRunRecord }>;

jobs:definition-changed

Fires when a job is created, edited, reset to its defaults or deleted; definition is null after a delete.

Payload: JobDefinitionChangedEvent

type AgentReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';

type JobRunsIn = 'workstream' | 'new-workstream';

type JobOutput = 'none' | 'draft' | 'merge';

type JobBuiltin =
	| 'name-chats'
	| 'name-services'
	| 'fix-checks'
	| 'update-from-main'
	| 'address-review'
	| 'check-compatibility'
	| 'update-docs';

type JobDefinitionRecord = Readonly<{
	id: string;
	name: string;
	instructions: string;
	model: string;
	effort: AgentReasoningEffort;
	runsIn: JobRunsIn;
	output: JobOutput;
	skillPresetId: string | null;
	builtin: JobBuiltin | null;
	createdAt: string;
	updatedAt: string;
}>;

type JobDefinitionChangedEvent = Readonly<{
	definitionId: string;
	definition: JobDefinitionRecord | null;
}>;

routines:suggested

Fires when Malini notices a prompt you keep repeating and suggests turning it into a routine.

Payload: RoutineSuggestedEvent

type RoutineSuggestionStatus = 'open' | 'dismissed' | 'accepted';

type RoutinePromptEvidence = Readonly<{
	kind: 'prompt';
	text: string;
	runId?: string;
	sessionId?: string;
	workstreamId?: string;
	submittedAt?: string;
}>;

type RoutineSuggestionRecord = Readonly<{
	id: string;
	clusterKey: string;
	status: RoutineSuggestionStatus;
	evidence: readonly RoutinePromptEvidence[];
	createdAt: string;
	updatedAt: string;
}>;

type RoutineSuggestedEvent = Readonly<{
	suggestion: RoutineSuggestionRecord;
}>;

routines:changed

Fires when a routine is created, updated, promoted, demoted or deleted; routine is null after a delete.

Payload: RoutineChangedEvent

type RoutineChange = 'created' | 'updated' | 'promoted' | 'demoted' | 'deleted';

type RoutineStatus = 'draft' | 'candidate' | 'routine';

type RoutineOrigin = 'user' | 'suggested';

type RoutineJsonValue =
	| string
	| number
	| boolean
	| null
	| readonly RoutineJsonValue[]
	| { readonly [key: string]: RoutineJsonValue };

type RoutineWorkflowRun = Readonly<{
	workflow: string;
	input: Readonly<Record<string, RoutineJsonValue>>;
}>;

type RoutineCommandRun = Readonly<{
	command: string;
	args: readonly RoutineJsonValue[];
}>;

type RoutineJobRun = Readonly<{ job: string }>;

type RoutineRun = RoutineWorkflowRun | RoutineCommandRun | RoutineJobRun;

type RoutinePromptEvidence = Readonly<{
	kind: 'prompt';
	text: string;
	runId?: string;
	sessionId?: string;
	workstreamId?: string;
	submittedAt?: string;
}>;

type RoutineRunEvidence = Readonly<{
	kind: 'run';
	runId: string;
	sessionId?: string;
	workstreamId?: string;
	summary?: string;
}>;

type RoutineEvidence = RoutinePromptEvidence | RoutineRunEvidence;

type RoutineRecord = Readonly<{
	id: string;
	status: RoutineStatus;
	origin: RoutineOrigin;
	label: string;
	when: string;
	steps: readonly RoutineRun[];
	evidence: readonly RoutineEvidence[];
	createdAt: string;
	updatedAt: string;
}>;

type RoutineChangedEvent = Readonly<{
	routineId: string;
	change: RoutineChange;
	routine: RoutineRecord | null;
}>;

routines:gated-run-changed

Fires when a routine run waits for your confirmation, and again when you confirm or reject it.

Payload: RoutineGatedRunChangedEvent

type RoutineJsonValue =
	| string
	| number
	| boolean
	| null
	| readonly RoutineJsonValue[]
	| { readonly [key: string]: RoutineJsonValue };

type RoutineGatedRunState = 'pending' | 'confirmed' | 'rejected';

type RoutineGatedRunRecord = Readonly<{
	id: string;
	routineId: string;
	workstreamId: string;
	runKey: string;
	event: string;
	payload: RoutineJsonValue;
	state: RoutineGatedRunState;
	createdAt: string;
	decidedAt: string | null;
}>;

type RoutineGatedRunChangedEvent = Readonly<{
	gatedRun: RoutineGatedRunRecord;
}>;

app:close-requested

Fires when you close the Malini window, so Malini can show what is still running and ask before it quits.

Payload: EmptyEventPayload

type EmptyEventPayload = Readonly<Record<string, never>>;

app:scale-changed

Fires when the interface scale or the display the window sits on changes, so layouts can measure again.

Payload: EmptyEventPayload

type EmptyEventPayload = Readonly<Record<string, never>>;

app:notification-opened

Fires when you click a Malini notification, with the chat it points to.

Payload: NotificationTarget

type NotificationTarget = Readonly<{ workstreamId: string; sessionId: string }>;

app:streaming-changed

Fires when streamer mode turns on or off, because a screen recording or streaming app started or stopped.

Payload: StreamingChangedPayload

type StreamingChangedPayload = Readonly<{ streaming: boolean }>;

app:updated

Fires once at startup after Malini updated to a new version whose events changed, with the old and new version and the events added, removed or whose payload changed.

Payload: AppUpdatedPayload

type ContractPayloadChange = Readonly<{ channel: string; before: string; after: string }>;

type ContractChanges = Readonly<{
	added: readonly string[];
	removed: readonly string[];
	changed: readonly ContractPayloadChange[];
}>;

type AppUpdatedPayload = Readonly<{ from: string; to: string; changes: ContractChanges }>;

app:update-staged

Fires when a new Malini version has downloaded and installs on the next restart, with that version.

Payload: UpdateStagedPayload

type UpdateStagedPayload = Readonly<{ version: string }>;

browser:state-changed

Fires whenever a workstream's browser tab changes its page, title, icon, loading state or history.

Payload: BrowserState

type BrowserState = Readonly<{
	workstreamId: string;
	tabId: string;
	url: string;
	title: string;
	favicon: string | null;
	loading: boolean;
	canGoBack: boolean;
	canGoForward: boolean;
}>;

browser:tab-opened

Fires when a new browser tab opens in a workstream, for example when an agent opens a page.

Payload: BrowserTabArgs

type BrowserTabArgs = Readonly<{ workstreamId: string; tabId: string }>;

stats:resources-sampled

Fires every few seconds with a sample of the memory and CPU that Malini, its agents, browser tabs and containers use.

Payload: ResourceSample

type ResourceKind = 'malini' | 'browser' | 'agents' | 'tools' | 'docker';

type ResourceUsage = Readonly<{
	kind: ResourceKind;
	memoryBytes: number;
	cpuPercent: number;
	processes: number;
}>;

type ResourceSample = Readonly<{
	sampledAt: number;
	memoryBytes: number;
	cpuPercent: number;
	processes: number;
	usage: readonly ResourceUsage[];
}>;

stats:workstream-running-changed

Fires when what keeps running in any workstream changes, with every workstream that still runs background services, tasks or containers.

Payload: readonly WorkstreamRunning[]

type RunningItemOrigin = 'agent' | 'docker';

type RunningItemKind = 'service' | 'task';

type RunningItem = Readonly<{
	id: string;
	origin: RunningItemOrigin;
	name: string;
	kind: RunningItemKind;
	detail: string;
	group: string | null;
	startedAt: number;
	ports: readonly number[];
}>;

type WorkstreamTurn = Readonly<{
	endedAt: number;
	outcome: string | null;
	need: string | null;
}>;

type WorkstreamRunning = Readonly<{
	workstreamId: string;
	items: readonly RunningItem[];
	behind: readonly string[];
	turn: WorkstreamTurn | null;
}>;