> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goldsky.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> How Compose context functions provide auditable, retryable, and durable access to the outside world from sandboxed tasks.

Context functions allow task sandboxes to access the outside world.

Context functions support individual retry configuration and are automatically logged for auditing and debugging. All communication outside of the task sandbox happens via Context Functions, making Compose apps run deterministic, given the same world context.

## Retry Configuration

All Context functions accept an optional retry configuration:

```typescript theme={null}
type ContextFunctionRetryConfig = {
  max_attempts: number; // 3 for read-only calls, 1 for mutations
  initial_interval_ms: number; // Default: 500
  backoff_factor: number; // Default: 2
};
```

**How Context Function Retries Work:**

* Each context function call can have its own retry configuration
* If a context function fails, it retries according to its configuration
* If all context function retries are exhausted, the context function will throw
* If you don't catch a failed context function call, a task-level retry may trigger, restarting the task and any context-function retries

## Durable execution and context caching

Compose provides durable execution guarantees. All context function calls are deterministically cached — if a task is interrupted (e.g. by a restart or deployment) and resumed, context functions that already completed will return their cached results instead of re-executing. This means:

* Transactions that already succeeded will not be re-sent
* Fetch calls that already returned will not be re-made
* Collection operations that already completed will not be repeated

This caching is scoped to each individual task run. Cached results are automatically cleaned up when the run completes (success or failure).

Non-deterministic values, such as timestamps or random IDs, must be produced through [ctx.sideEffect()](./side-effect) so a replay returns the same value instead of recomputing it.

<Note>
  By default, read-only context calls make up to 3 attempts (`max_attempts: 3`, `initial_interval_ms: 500`, `backoff_factor: 2`). This covers `readContract`, `simulate`, `getBalance`, and `ctx.fetch` with `GET`, `HEAD`, or `OPTIONS`. Mutations such as `callTask`, `writeContract`, `sendTransaction`, and `ctx.fetch` with `POST`, `PUT`, `PATCH`, or `DELETE` default to a single attempt (`max_attempts: 1`). Pass a `retryConfig` to override either default.
</Note>

## Overview of key context properties and functions

Here's a brief overview of what context enables. Use the left nav or links below to find full reference docs.

* State Management - [Collections](./collections)
* Blockchain Interactions - [evm](./evm/overview)
* HTTP requests - [fetch](./fetch)
* Task to task execution - [callTask](./call-task)
* Non-deterministic values - [sideEffect](./side-effect)
* Reading env variables - [env](./env)
* Reorg handling - [Reorgs](./evm/reorgs)
* Turbo pipeline management - [turbo](./turbo)
* Direct SQL access - [db](./db)
* Run-aware logging - [logger](#logger)

## Full TaskContext interface

```typescript theme={null}
export type ContextFunctionRetryConfig = {
  max_attempts: number;
  initial_interval_ms: number;
  backoff_factor: number;
};

export type Chain = {
  id: number;
  name: string;
  testnet: boolean;
  nativeCurrency: {
    name: string;
    symbol: string;
    decimals: number;
  };
  rpcUrls: {
    public: { http: string[] };
    default: { http: string[] };
  };
  blockExplorers: {
    default: { name: string; url: string };
  };
  contracts?: Record<string, { address: string }>;
};

export type ScalarIndexType = "text" | "numeric" | "boolean" | "timestamptz";

export interface CollectionIndexSpec {
  path: string;
  type: ScalarIndexType;
  unique?: boolean;
}

export interface FindOptions {
  limit?: number;
  offset?: number;
}

// Filter helpers for comparison operators
export type FilterHelper =
  | "$gt"
  | "$gte"
  | "$lt"
  | "$lte"
  | "$in"
  | "$ne"
  | "$nin"
  | "$exists";
export type HelperValue = Partial<
  Record<FilterHelper, string | number | boolean | string[] | number[]>
>;
export type FilterValue = string | number | boolean | HelperValue;
export type Filter = Record<string, FilterValue>;

export type WithId<T> = T & { id: string };

export interface Collection<TDoc = unknown> {
  readonly name: string;
  insertOne(doc: TDoc, opts?: { id?: string }): Promise<{ id: string }>;
  findOne(filter: Filter): Promise<WithId<TDoc> | null>;
  findMany(filter: Filter, options?: FindOptions): Promise<Array<WithId<TDoc>>>;
  getById(id: string): Promise<WithId<TDoc> | null>;
  /**
   * @param opts.upsert - Defaults to true. Set to false to throw if document doesn't exist.
   */
  setById(
    id: string,
    doc: TDoc,
    opts?: { upsert?: boolean },
  ): Promise<{ id: string; upserted?: boolean; matched?: number }>;
  deleteById(id: string): Promise<{ deletedCount: number }>;
  drop(): Promise<void>;
}

export type Address = `0x${string}`;

export interface WalletConfig {
  name?: string; // defaults to "default"
  privateKey?: string;
  sponsorGas?: boolean; // defaults to true for Privy wallets, false for private key wallets
}

export interface WebhookWalletConfig {
  url: string;
  address: Address;
  headers?: Record<string, string>;
  name?: string;
}

export type ReplayOnReorg = {
  type: "replay";
};

export type LogOnReorg = {
  type: "log";
  logLevel?: "error" | "info" | "warn"; // defaults to "error"
};

export type CustomReorgAction = {
  type: "task";
  // your task will be sent with a payload the full transaction minus gas and nonce
  task: string;
};

export type OnReorgOptions = ReplayOnReorg | LogOnReorg | CustomReorgAction;

export type OnReorgConfig = {
  action: OnReorgOptions;
  depth: number;
};

export interface TransactionConfirmation {
  // this the number of block confirmations before we resolve the promise
  // i.e. "wait 5 blocks before proceeding to the next step in my task"
  confirmations?: number;
  onReorg?: OnReorgConfig;
}

export interface IWallet {
  readonly name: string;
  readonly address: Address;
  writeContract(
    chain: Chain,
    contractAddress: Address,
    functionSig: string,
    args: unknown[],
    confirmation?: TransactionConfirmation,
    retryConfig?: ContextFunctionRetryConfig,
  ): Promise<{
    hash: string;
    receipt: TransactionReceipt;
    userOpHash?: string; // set for gas-sponsored transactions (ERC-4337)
  }>;
  readContract<T = unknown>(
    chain: Chain,
    contractAddress: Address,
    functionSig: string,
    args: unknown[],
    retryConfig?: ContextFunctionRetryConfig,
  ): Promise<T>;
  sendTransaction(
    config: {
      to: Address;
      data: `0x${string}`;
      chain: Chain;
      value?: bigint;
      maxFeePerGas?: bigint;
      maxPriorityFeePerGas?: bigint;
      gas?: bigint;
      nonce?: number; // EOA wallets only
    },
    confirmation?: TransactionConfirmation,
    retryConfig?: ContextFunctionRetryConfig,
  ): Promise<{
    hash: string;
    receipt: TransactionReceipt;
    userOpHash?: string; // set for gas-sponsored transactions (ERC-4337)
  }>;
  simulate(
    chain: Chain,
    contractAddress: Address,
    functionSig: string,
    args: unknown[],
    retryConfig?: ContextFunctionRetryConfig,
  ): Promise<{ hash: string }>;
  getBalance(
    chain: Chain,
    retryConfig?: ContextFunctionRetryConfig,
  ): Promise<string>; // native token balance in wei
}

export interface Log {
  address: Address;
  topics: `0x${string}`[];
  data: `0x${string}`;
  blockHash: `0x${string}`;
  blockNumber: bigint;
  logIndex: number;
  transactionHash: `0x${string}`;
  transactionIndex: number;
  removed?: boolean;
}

export interface TransactionReceipt {
  blockHash: `0x${string}`;
  blockNumber: bigint;
  contractAddress: Address | null;
  cumulativeGasUsed: bigint;
  effectiveGasPrice: bigint;
  from: Address;
  gasUsed: bigint;
  logs: Log[];
  logsBloom: `0x${string}`;
  status: "success" | "reverted";
  to: Address | null;
  transactionHash: `0x${string}`;
  transactionIndex: number;
  type: "legacy" | "eip1559" | "eip2930" | "eip4844" | "eip7702";
}

export interface OnchainEvent {
  blockNumber: number;
  blockHash: string;
  transactionIndex: number;
  removed: boolean;
  address: string;
  data: `0x${string}`;
  topics: `0x${string}`[];
  transactionHash: string;
  logIndex: number;
}

export interface FetchConfig {
  method?: string;
  headers?: Record<string, string>;
  body?: Record<string, unknown> | string;
}

export interface Logger {
  info(message: string, data?: Record<string, unknown>): void;
  warn(message: string, data?: Record<string, unknown>): void;
  error(message: string, data?: Record<string, unknown>): void;
}

export type TaskContext = {
  env: Record<string, string>;
  logger: Logger;
  callTask: <Args = Record<string, unknown>, T = unknown>(
    taskName: string,
    args: Args,
    retryConfig?: ContextFunctionRetryConfig,
  ) => Promise<T>;
  sideEffect: <T>(fn: () => T | Promise<T>) => Promise<T>;
  fetch: <T = unknown>(
    url: string,
    fetchConfigOrRetryConfig?: FetchConfig | ContextFunctionRetryConfig,
    retryConfig?: ContextFunctionRetryConfig,
  ) => Promise<T | undefined>;
  evm: {
    chains: Record<string, Chain>;
    wallet: (config: WalletConfig) => Promise<IWallet>;
    webhookWallet: (config: WebhookWalletConfig) => Promise<IWallet>;
    decodeEventLog: <T = unknown>(abi: Abi, log: OnchainEvent) => Promise<T>;
    contracts: Record<string, unknown>; // auto-generated from ABIs in src/contracts/
  };
  collection: <T>(
    name: string,
    indexes?: CollectionIndexSpec[],
  ) => Promise<Collection<T>>;
  turbo: {
    createPipeline: (config: CreateTurboPipelineConfig, retryConfig?: ContextFunctionRetryConfig) => Promise<TurboPipeline>;
    createJob: (config: CreateTurboJobConfig, retryConfig?: ContextFunctionRetryConfig) => Promise<TurboPipeline>;
    getPipeline: (name: string, retryConfig?: ContextFunctionRetryConfig) => Promise<TurboPipeline>;
    getStatus: (name: string, retryConfig?: ContextFunctionRetryConfig) => Promise<TurboPipelineStatus>;
    listPipelines: (retryConfig?: ContextFunctionRetryConfig) => Promise<TurboPipeline[]>;
    pausePipeline: (name: string, retryConfig?: ContextFunctionRetryConfig) => Promise<PipelineActionResult>;
    resumePipeline: (name: string, retryConfig?: ContextFunctionRetryConfig) => Promise<PipelineActionResult>;
    restartPipeline: (name: string, options?: RestartOptions, retryConfig?: ContextFunctionRetryConfig) => Promise<PipelineActionResult>;
    deletePipeline: (name: string, retryConfig?: ContextFunctionRetryConfig) => Promise<PipelineDeleteResult>;
    getLogs: (name: string, options?: GetLogsOptions, retryConfig?: ContextFunctionRetryConfig) => Promise<PipelineLogsResult>;
    getState: (name: string, retryConfig?: ContextFunctionRetryConfig) => Promise<unknown>;
    validate: (definition: TurboPipelineDefinition, retryConfig?: ContextFunctionRetryConfig) => Promise<ValidateResult>;
  };
  db: {
    query: <T = Record<string, unknown>>(
      schema: string,
      sql: string,
      params?: (string | number | boolean | null)[],
      retryConfig?: ContextFunctionRetryConfig,
    ) => Promise<DbQueryResult<T>>;
  };
};
```

See the [turbo reference](./turbo) for the full type definitions used above (`TurboPipeline`, `TurboPipelineStatus`, `ValidateResult`, etc.), and the [db reference](./db) for `DbQueryResult`.

## Logger

`context.logger` is a run-aware structured logger. Unlike `console.log`, logs emitted through `context.logger` are tagged with the current task name and run ID, which enables two powerful debugging workflows in the Compose dashboard:

1. **Search app-level logs, then jump to the run** — On the Logs tab of your app, you can search for a log pattern across all runs. Each matching log entry includes a "View run" link that takes you directly to the full task run, where you can see the complete OpenTelemetry trace of everything that happened in that run.

2. **View run-specific logs** — On any task run's detail page, the Logs tab shows only logs from that specific run, making it easy to understand what happened in a single execution.

### Usage

```typescript theme={null}
import { TaskContext } from "compose";

export async function main({ logger, evm, env }: TaskContext) {
  logger.info("Starting price update", { source: "coingecko" });

  try {
    const wallet = await evm.wallet();
    const { hash } = await wallet.writeContract(
      evm.chains.base,
      env.CONTRACT_ADDRESS as `0x${string}`,
      "setPrice(bytes32,bytes32)",
      [timestampBytes, priceBytes]
    );

    logger.info("Price updated", { hash, chain: "base" });
  } catch (error) {
    logger.error("Price update failed", {
      error: error instanceof Error ? error.message : String(error),
    });
    throw error;
  }
}
```

### API

```typescript theme={null}
interface Logger {
  info(message: string, data?: Record<string, unknown>): void;
  warn(message: string, data?: Record<string, unknown>): void;
  error(message: string, data?: Record<string, unknown>): void;
}
```

Each method takes a message string and an optional `data` object for structured metadata. BigInt values (common in EVM code) are automatically serialized as strings.

### `console.log` vs `context.logger`

Both `console.log` and `context.logger` output to your terminal during local development. The difference is in the cloud:

|                                  | `console.log` | `context.logger` |
| -------------------------------- | ------------- | ---------------- |
| Appears in app-level logs        | Yes           | Yes              |
| Tagged with run ID and task name | No            | Yes              |
| "View run" link in dashboard     | No            | Yes              |
| Appears in run-specific Logs tab | No            | Yes              |
| Structured `data` field          | No            | Yes              |

Use `console.log` for quick debugging. Use `context.logger` when you want logs that you can trace back to a specific task run in production.

## Next Steps

<CardGroup cols={2}>
  <Card title="Fetch" icon="code" href="./fetch">
    Make auditable HTTP requests with fetch.
  </Card>

  <Card title="Collections" icon="file-code" href="./collections">
    Manage state across tasks and task runs with collections.
  </Card>
</CardGroup>


## Related topics

- [Overview](/chains/introduction.md)
- [Health dashboard](/turbo-pipelines/health-dashboard.md)
- [Agent Skills](/ai-skills.md)
- [Teams and projects](/teams-and-projects.md)
