Overview
Executor uses a hierarchical isolation model with Organizations as the billing and membership umbrella, and Workspaces as the unit of execution isolation for tasks, tools, and credentials.
Organizations
Organizations represent a billing entity and membership boundary:
// From schema.ts:90-102
organizations: defineTable({
workosOrgId: v.optional(v.string()), // External WorkOS org ID
slug: v.string(), // URL-friendly identifier
name: v.string(),
status: organizationStatusValidator, // "active" | "deleted"
createdByAccountId: v.optional(v.id("accounts")),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_workos_org_id", ["workosOrgId"])
.index("by_slug", ["slug"])
.index("by_status_created", ["status", "createdAt"])
Organization Properties
URL-friendly identifier, globally unique. Used in URLs and API paths.
Display name for the organization.
status
'active' | 'deleted'
required
Organization lifecycle status. Only "active" organizations can be used.
External WorkOS organization ID for SSO and directory sync integration.
Account that created the organization.
Workspaces
Workspaces are the primary unit of isolation. Each workspace belongs to exactly one organization:
// From schema.ts:69-82
workspaces: defineTable({
organizationId: v.id("organizations"),
slug: v.string(), // Unique within organization
name: v.string(),
iconStorageId: v.optional(v.id("_storage")),
createdByAccountId: v.optional(v.id("accounts")),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_organization", ["organizationId"])
.index("by_organization_created", ["organizationId", "createdAt"])
.index("by_organization_slug", ["organizationId", "slug"])
.index("by_slug", ["slug"]) // Global slug resolution
Workspace Properties
URL-friendly identifier. Must be unique within the organization. Can be globally unique for direct resolution.
Display name for the workspace.
organizationId
Id<'organizations'>
required
Parent organization this workspace belongs to.
Optional custom icon stored in Convex storage.
Account that created the workspace.
Isolation Boundaries
Workspaces provide isolation for:
Tasks
Tool Sources
Credentials
Approvals
Storage
Every task belongs to a workspace:tasks: defineTable({
taskId: v.string(),
workspaceId: v.id("workspaces"), // Workspace isolation
accountId: v.optional(v.id("accounts")),
// ...
})
Tasks cannot access resources from other workspaces. Tool sources can be scoped to workspace or organization:toolSources: defineTable({
sourceId: v.string(),
scopeType: toolSourceScopeTypeValidator, // "workspace" | "organization"
organizationId: v.id("organizations"),
workspaceId: v.optional(v.id("workspaces")),
// ...
})
- Workspace-scoped: Only visible in that workspace
- Organization-scoped: Shared across all workspaces in the org
Credentials support three scope levels:sourceCredentials: defineTable({
credentialId: v.string(),
scopeType: credentialScopeTypeValidator, // "account" | "workspace" | "organization"
accountId: v.optional(v.id("accounts")),
workspaceId: v.optional(v.id("workspaces")),
organizationId: v.id("organizations"),
// ...
})
- Account: Personal credentials, only for that user
- Workspace: Shared within the workspace
- Organization: Shared across all workspaces in the org
Approvals are workspace-scoped:approvals: defineTable({
approvalId: v.string(),
workspaceId: v.id("workspaces"), // Workspace isolation
taskId: v.string(),
// ...
})
Only workspace admins can resolve approvals. Storage instances support four scope levels:storageInstances: defineTable({
instanceId: v.string(),
scopeType: storageScopeTypeValidator, // "scratch" | "account" | "workspace" | "organization"
organizationId: v.id("organizations"),
workspaceId: v.optional(v.id("workspaces")),
accountId: v.optional(v.id("accounts")),
// ...
})
- Scratch: Ephemeral, task-specific
- Account: Personal storage
- Workspace: Shared workspace storage
- Organization: Shared org-wide storage
Organization Membership
Members are linked to organizations with role-based access:
// From schema.ts:111-125
organizationMembers: defineTable({
organizationId: v.id("organizations"),
accountId: v.id("accounts"),
role: orgRoleValidator, // "owner" | "admin" | "member" | "billing_admin"
status: orgMemberStatusValidator, // "active" | "pending" | "removed"
billable: v.boolean(), // Counts towards seat billing
invitedByAccountId: v.optional(v.id("accounts")),
joinedAt: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_org", ["organizationId"])
.index("by_org_account", ["organizationId", "accountId"])
.index("by_org_billable_status", ["organizationId", "billable", "status"])
Organization Roles
Full administrative access, can delete organization, manage billing.
Administrative access, can manage members, workspaces, and settings.
Standard access, can execute tasks and view workspace resources.
Can manage billing and subscription settings.
Billable Seats
The billable flag controls whether a member counts towards the organization’s seat count:
// From schema.ts:116
billable: v.boolean(), // Counts towards seat billing
Access Pattern: Count billable active members via by_org_billable_status index:
.withIndex("by_org_billable_status", (q) =>
q.eq("organizationId", orgId)
.eq("billable", true)
.eq("status", "active")
)
Invitations
Organizations can invite users via email:
// From schema.ts:133-148
invites: defineTable({
organizationId: v.id("organizations"),
workspaceId: v.optional(v.id("workspaces")), // Optional workspace assignment
email: v.string(),
role: orgRoleValidator,
status: inviteStatusValidator, // "pending" | "accepted" | "expired" | "revoked" | "failed"
providerInviteId: v.optional(v.string()), // External WorkOS invite ID
invitedByAccountId: v.id("accounts"),
expiresAt: v.number(),
acceptedAt: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
})
Invites can optionally pre-assign users to a specific workspace. If workspaceId is set, the user joins both the organization and the designated workspace.
Access Patterns
Common workspace and organization queries:
Resolve Workspace by Slug
// Global slug lookup
await ctx.db
.query("workspaces")
.withIndex("by_slug", (q) => q.eq("slug", "my-workspace"))
.unique();
// Organization-scoped slug lookup
await ctx.db
.query("workspaces")
.withIndex("by_organization_slug", (q) =>
q.eq("organizationId", orgId).eq("slug", "my-workspace")
)
.unique();
List Workspaces in Organization
// Ordered by creation time (newest first)
await ctx.db
.query("workspaces")
.withIndex("by_organization_created", (q) =>
q.eq("organizationId", orgId)
)
.order("desc")
.collect();
Check Organization Membership
// From policies.ts:241-256
const membership = await ctx.db
.query("organizationMembers")
.withIndex("by_org_account", (q) =>
q.eq("organizationId", orgId).eq("accountId", accountId)
)
.unique();
if (!membership || membership.status !== "active") {
throw new Error("Account is not an active member");
}
Dashboard Workflows
Organization Management
- Create organization with name and slug
- Invite members via email with role assignment
- Manage billing through Stripe integration
- View usage metrics across all workspaces
Workspace Management
- Create workspace within organization
- Configure tool sources (workspace or org-scoped)
- Manage credentials (account, workspace, or org-scoped)
- Set access policies for tool usage
- Monitor tasks and approvals
Multi-Tenancy
Executor’s workspace model supports several tenancy patterns:
Pattern 1: Single Organization, Multiple Workspaces
Organization: Acme Corp
├── Workspace: Production
│ ├── Tool Sources (production APIs)
│ ├── Credentials (production keys)
│ └── Access Policies (require approval)
├── Workspace: Staging
│ ├── Tool Sources (staging APIs)
│ └── Access Policies (auto-approve)
└── Workspace: Development
└── Access Policies (allow all)
Use for: Environment separation (dev/staging/prod)
Pattern 2: Multiple Organizations
Organization: Customer A
└── Workspace: Customer A Workspace
Organization: Customer B
└── Workspace: Customer B Workspace
Use for: Complete customer isolation in SaaS deployments
Pattern 3: Organization-Scoped Resources
Organization: Acme Corp
├── Tool Source: Shared GitHub (org-scoped)
├── Credential: Shared API Key (org-scoped)
├── Workspace: Team A
│ └── Inherits org-scoped resources
└── Workspace: Team B
└── Inherits org-scoped resources
Use for: Sharing common tools across teams
Best Practices
Workspace Design
- Create separate workspaces for different environments (dev/staging/prod)
- Use organization-scoped resources for shared tools and credentials
- Name workspaces clearly to reflect their purpose
Organization Structure
- One organization per company/customer in SaaS deployments
- Use workspace-scoped credentials for sensitive environment-specific keys
- Assign appropriate roles to members based on responsibilities
Access Control
- Set restrictive policies in production workspaces
- Use approval workflows for destructive operations
- Grant admin role sparingly, use member role for most users
- Tasks - Task execution within workspaces
- Tools - Workspace and organization-scoped tool sources
- Credentials - Multi-scope credential management
- Policies - Workspace access policies