langgenius/dify · critical · ValueError

Workspace owner not found for tenant={current_tenant_id}

Error message

Workspace owner not found for tenant={current_tenant_id}

What it means

ValueError raised inside _iter_tenant_member_batches' flush_current_tenant closure when a tenant's accumulated rows contain no member whose role equals TenantAccountRole.OWNER.value. Every workspace is expected to have exactly one owner; without one, the migration cannot determine the operator account that must authorize member-role replacements.

Source

Thrown at api/commands/rbac.py:102

            select(TenantAccountJoin.tenant_id, TenantAccountJoin.account_id, TenantAccountJoin.role)
            .order_by(TenantAccountJoin.tenant_id.asc(), TenantAccountJoin.id.asc())
            .execution_options(yield_per=db_batch_size)
        )
        if tenant_id:
            stmt = stmt.where(TenantAccountJoin.tenant_id == tenant_id)

        current_tenant_id: str | None = None
        owner_account_id: str | None = None
        batches: list[list[tuple[str, str]]] = []
        batch: list[tuple[str, str]] = []

        def flush_current_tenant() -> Iterator[tuple[str, str, list[tuple[str, str]]]]:
            if current_tenant_id is None:
                return
            if batch:
                batches.append(batch.copy())
            if not owner_account_id:
                raise ValueError(f"Workspace owner not found for tenant={current_tenant_id}")
            for item in batches:
                yield current_tenant_id, owner_account_id, item

        for row in session.execute(stmt):
            workspace_id = str(row.tenant_id)
            if current_tenant_id is not None and workspace_id != current_tenant_id:
                yield from flush_current_tenant()
                owner_account_id = None
                batches = []
                batch = []
            current_tenant_id = workspace_id
            account_id = str(row.account_id)
            role = str(row.role)
            if role == TenantAccountRole.OWNER.value:
                owner_account_id = account_id
            batch.append((account_id, role))
            if len(batch) >= api_batch_size:
                batches.append(batch)

View on GitHub (pinned to ef8544b173)

Solutions

  1. Query `SELECT * FROM tenant_account_joins WHERE tenant_id='<id>' AND role='owner';` to confirm the owner row is missing.
  2. Restore or designate an owner for the tenant before re-running the migration.
  3. If the workspace is defunct, exclude its tenant_id via the command's --tenant-id filter.
  4. Audit for role string drift (e.g. 'Owner' vs 'owner') caused by case-sensitive comparisons.

Example fix

-- before
-- SELECT role, count(*) FROM tenant_account_joins WHERE tenant_id='t1' GROUP BY role;
--  admin   | 2
--  normal  | 5

-- after - restore owner
UPDATE tenant_account_joins SET role='owner' WHERE tenant_id='t1' AND account_id='<owner-acct>';
Defensive patterns

Strategy: validation

Validate before calling

def tenant_has_owner(session, tenant_id: str) -> bool:
    from sqlalchemy import select, func
    return bool(session.scalar(
        select(func.count()).select_from(TenantAccountJoin)
        .where(TenantAccountJoin.tenant_id == tenant_id,
               TenantAccountJoin.role == TenantAccountRole.OWNER.value)
    ))

Type guard

def tenant_has_owner(session, tenant_id: str) -> bool:
    return session.scalars(
        select(TenantAccountJoin).where(
            TenantAccountJoin.tenant_id == tenant_id,
            TenantAccountJoin.role == TenantAccountRole.OWNER.value,
        ).limit(1)
    ).first() is not None

Try / catch

try:
    for batch in _iter_tenant_member_batches(...):
        process(batch)
except ValueError as exc:
    if "Workspace owner not found" in str(exc):
        click.echo(f"Orphaned workspace: {exc}. Restoring owner or excluding tenant.", err=True)
    raise

Prevention

When it happens

Trigger: Triggered when, for a given tenant_id, none of the streamed TenantAccountJoin rows have role == 'owner'. This is evaluated at tenant-boundary flush time.

Common situations: The owner row was deleted from TenantAccountJoin (orphaned workspace), the owner's role string was changed to a non-owner value via a bad migration, or data corruption removed the ownership binding.

Related errors


AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12). Data as JSON: /api/errors/6d6d759f4e6e942f. Report an issue: GitHub.