Skip to main content

Overview

Tasks are the fundamental execution unit in Executor. Each task represents a single code execution within a workspace, tracked from creation through completion with full lifecycle management.

Task Lifecycle

Every task progresses through a series of statuses:
1

Queued

Task is created and waiting for execution. The task enters a queue and will be picked up by the runtime scheduler.Source: packages/database/convex/database/tasks.ts:80-92
2

Running

Task is actively executing in a runtime environment. Code is being executed and tool calls may be made.Source: packages/database/convex/database/tasks.ts:146-163
3

Terminal States

Task reaches one of four terminal states:
  • Completed: Execution finished successfully
  • Failed: Execution encountered an error
  • Timed Out: Execution exceeded the timeout limit
  • Denied: Execution was blocked by approval policy
Source: packages/core/src/types.ts:38-39

Creating Tasks

Tasks are created via the createTask workspace action:
// From executor.ts
export const createTask = workspaceAction({
  args: {
    code: v.string(),
    timeoutMs: v.optional(v.number()),
    runtimeId: v.optional(v.string()),
    metadata: v.optional(jsonObjectValidator),
    accountId: v.optional(vv.id("accounts")),
    waitForResult: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    return await createTaskHandler(ctx, internal, args);
  },
});

Task Properties

taskId
string
required
Unique domain identifier (e.g., task_<uuid>). This stable ID is used across systems and in logs.
code
string
required
The code to execute in the runtime environment.
runtimeId
string
required
Runtime environment identifier (e.g., cloudflare-worker, node).
workspaceId
Id<'workspaces'>
required
The workspace this task belongs to.
accountId
Id<'accounts'>
Optional account that initiated the task.
clientId
string
Client label such as "web", "mcp", etc.
timeoutMs
number
default:"120000"
Execution timeout in milliseconds. Clamped between 1ms and MAX_TASK_TIMEOUT_MS.Source: packages/database/convex/database/tasks.ts:46-56
metadata
Record<string, unknown>
default:"{}"
Arbitrary metadata attached to the task. Used for storage tracking, custom flags, etc.

Task Statuses

The task status determines its current execution state:
// Task is waiting for execution
status: "queued"
The task has been created and is in the execution queue. The scheduler will pick it up when capacity is available.

Task Events

All task activity is logged as events in an append-only log:
// Schema from schema.ts:278-287
taskEvents: defineTable({
  sequence: v.number(),        // Monotonic sequence per task
  taskId: v.string(),          // References tasks.taskId
  eventName: v.string(),       // Event type identifier
  type: v.string(),            // Event category
  payload: jsonObjectValidator, // Event-specific data
  createdAt: v.number(),
})
Events enable ordered replay of task execution history. Access Pattern: Index by (taskId, sequence) for efficient chronological retrieval.

Storage Integration

Tasks can interact with storage instances (filesystem, KV, SQL). Storage access is tracked in task metadata:
// From tasks.ts:239-297
export const trackTaskStorageAccess = internalMutation({
  args: {
    taskId: v.string(),
    instanceId: v.string(),
    scopeType: v.optional(storageScopeTypeValidator),
    accessType: storageAccessTypeValidator,
  },
  // Tracks: accessedInstanceIds, openedInstanceIds, providedInstanceIds
});
Storage tracking enables workspace administrators to see which tasks accessed which storage instances, supporting audit and cost allocation workflows.

Queue Management

The scheduler polls for queued tasks:
// From tasks.ts:122-133
export const listQueuedTaskIds = internalQuery({
  args: { limit: v.optional(v.number()) },
  handler: async (ctx, args) => {
    const docs = await ctx.db
      .query("tasks")
      .withIndex("by_status_created", (q) => q.eq("status", "queued"))
      .order("asc")
      .take(args.limit ?? 20);

    return docs.map((doc) => doc.taskId);
  },
});
Scheduling: Tasks are picked oldest-first (FIFO) from the queued status index.

Task Completion

When execution finishes, tasks transition to a terminal state:
// From tasks.ts:166-195
export const markTaskFinished = internalMutation({
  args: {
    taskId: v.string(),
    status: completedTaskStatusValidator, // completed | failed | timed_out | denied
    exitCode: v.optional(v.number()),
    error: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // Only transitions if not already in terminal state
    if (isTerminalTaskStatus(doc.status)) {
      return mapTask(doc);
    }
    
    await ctx.db.patch(doc._id, {
      status: args.status,
      exitCode: args.exitCode,
      error: args.error,
      completedAt: now,
      updatedAt: now,
    });
  },
});
Once a task reaches a terminal state, its status cannot be changed. This ensures execution history is immutable.

Best Practices

Timeout Configuration

  • Default timeout is 120 seconds (2 minutes)
  • Set shorter timeouts for quick operations to free up runtime capacity
  • Set longer timeouts for complex workflows, but be aware of resource usage

Metadata Usage

  • Use metadata to track custom workflow state
  • Store references to external systems (e.g., ticket IDs, user sessions)
  • Avoid storing large payloads; use storage instances instead

Client Identification

  • Always set clientId to identify the source of execution
  • Use consistent labels: "web", "mcp", "api", "cli"
  • Enables filtering and analytics by client type
  • Approvals - Approval workflows for sensitive operations
  • Workspaces - Workspace isolation and organization
  • Tools - Tool sources and discovery

Build docs developers (and LLMs) love