Governance API
The Governance module provides fine-grained access control and compliance features including role-based access control (RBAC), organization groups, destination policies, access policies, folder and destination access lists, and deletion rules.
Access to a workspace is granted only through a group: a group lives on an organization, holds people, and is granted roles on the workspaces it should reach. There is no direct workspace membership and no per-group permission grant.
Endpoint Groups
| Group | Description |
|---|---|
| Roles | System and custom role management |
| Permissions | Granular permission definitions |
| Groups | Organization-scoped group management |
| Grants | Which roles a group holds on which workspace |
| Group Members | Who is in a group |
| Destination Policies | Sync-time governance policies |
| Access Policies | Data row-level access restrictions |
| Deletion Rules | Audience-scoped warehouse deletion with audit logs |
Roles
A role is a named set of permissions. It is not held by a person — it is held by a grant, which gives one group one role on one workspace. Zeotap includes three built-in roles (owner, admin, member) and supports custom roles.
Custom roles belong to the organization, not to a workspace, so a role is written once and can be granted on every workspace in it.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/organizations/{orgId}/roles | List the built-in roles plus this organization’s custom roles |
POST | /api/v1/organizations/{orgId}/roles | Create a custom role |
PUT | /api/v1/organizations/{orgId}/roles/{roleId} | Update a custom role |
DELETE | /api/v1/organizations/{orgId}/roles/{roleId} | Delete a custom role |
GET | /api/v1/roles | List the built-in roles, with no organization context |
Creating, updating and deleting a role requires roles.write, which in practice means membership of the organization’s administrators group.
List Roles
GET /api/v1/organizations/{orgId}/roles
[
{
"id": "00000000-0000-0000-0000-000000000001",
"organization_id": null,
"name": "owner",
"description": "Full access including workspace deletion",
"is_system": true,
"permissions": ["sources.read", "sources.write", "models.read", "..."],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "880e8400-e29b-41d4-a716-446655440000",
"name": "data_analyst",
"description": "Read-only access to models and audiences",
"is_system": false,
"permissions": ["sources.read", "models.read", "audiences.read"],
"created_at": "2024-01-15T09:30:00Z",
"updated_at": "2024-01-15T09:30:00Z"
}
]Create Custom Role
POST /api/v1/organizations/{orgId}/roles
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Role name, unique within the organization. owner, admin and member are reserved |
description | string | No | Description of the role’s purpose |
permissions | array of strings | Yes | Permission keys to grant. Each is matched against the catalog exactly, so list the individual keys a category expands to |
{
"name": "data_analyst",
"description": "Read-only access to models and audiences",
"permissions": [
"sources.read",
"models.read",
"audiences.read",
"traits.read"
]
}System Roles
| Role | ID | Description | Permissions |
|---|---|---|---|
owner | 00000000-0000-0000-0000-000000000001 | Full access including workspace deletion | 119 of 120 |
admin | 00000000-0000-0000-0000-000000000002 | Full access except workspace deletion | 118 of 120 |
member | 00000000-0000-0000-0000-000000000003 | Read-only access | 49 of 120 |
System roles carry a null organization_id and is_system: true, and cannot be edited or deleted.
Deleting a custom role does not fail when grants still hold it. Every grant carrying the role is moved to the built-in member role first, so those groups keep reaching their workspaces with reduced permissions rather than losing access outright. Check who holds the role before deleting it.
Permissions
Permissions are granular access control strings organized by category.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/permissions | List all available permissions (global catalog) |
GET | /api/v1/workspaces/{id}/permissions/me | Get the caller’s permissions in this workspace |
Response
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"key": "sources.read",
"category": "sources",
"description": "View sources and connection details",
"created_at": "2024-01-01T00:00:00Z"
},
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"key": "sources.write",
"category": "sources",
"description": "Create and update sources",
"created_at": "2024-01-01T00:00:00Z"
}
]Permission Keys
Keys are {category}.{action}. Most categories pair read with write, where write covers creating, editing and deleting. Where one action has to be grantable on its own it gets its own key — syncs.trigger, stores.trigger, deletion_rules.execute, warehouse_users.rotate, workspace_audit.export, events.debug. A category with sub-resources nests them before the action, as events.contracts.read does.
The catalog holds 120 permissions across 43 categories. GET /api/v1/permissions returns every row, which is the authoritative list; the permissions reference is the same catalog with the role matrix beside it.
Groups
A group lives on an organization, holds people, and is granted roles on the workspaces it should reach. Groups are the only way to grant access to a workspace — there is no direct workspace membership.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/organizations/{orgId}/groups | List the organization’s groups |
POST | /api/v1/organizations/{orgId}/groups | Create a group |
GET | /api/v1/organizations/{orgId}/groups/{groupId} | Get a group, with its grants |
PUT | /api/v1/organizations/{orgId}/groups/{groupId} | Rename a group |
DELETE | /api/v1/organizations/{orgId}/groups/{groupId} | Delete a group |
GET | /api/v1/workspaces/{id}/groups | List the grants reaching this workspace — the read behind the workspace Groups tab |
Create Group
POST /api/v1/organizations/{orgId}/groups
{
"name": "Marketing Team",
"description": "Marketing department members"
}A group carries no role of its own. Roles are attached per workspace, through a grant — see Grants below.
Group Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
organization_id | string (UUID) | Owning organization |
name | string | Group name, unique within the organization (case-insensitive) |
description | string | Description |
system_key | string | org_admins or org_viewers for the two groups every organization is seeded with, absent for every other group. System groups cannot be renamed or deleted |
member_count | integer | Number of people in the group |
workspace_count | integer | How many distinct workspaces the group reaches. A group holding two roles on one workspace reaches one workspace |
org_wide | boolean | true when the group holds an organization-wide grant — every workspace in the organization, including ones created later |
assignments | array of grants | Returned by the detail paths, not by list |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Grants
A grant is one group, one workspace, and the roles it holds there. Several roles are allowed, and the permissions they carry are unioned.
| Method | Path | Description |
|---|---|---|
PUT | /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId} | Set the roles this group holds on this workspace |
DELETE | /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId} | Revoke the group’s access to this workspace |
Set Workspace Roles
PUT /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId}
{
"role_ids": [
"00000000-0000-0000-0000-000000000003",
"550e8400-e29b-41d4-a716-446655440000"
]
}The body states the complete set of roles the group should hold on that workspace — roles you leave out are removed. An empty or absent role_ids attaches the group to the workspace granting nothing, which is how a group carries an access policy without carrying a role. The single-role spelling {"role_id": "..."} is still accepted; role_ids wins when both are sent.
Grant Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
group_id | string (UUID) | The group holding the grant |
group_name | string | Joined for display |
organization_id | string (UUID) | The organization both sides belong to |
workspace_id | string (UUID) or absent | The workspace reached. Absent means every workspace in the organization, now and in future — the organization-wide grant the system groups hold |
workspace_name | string | Joined for display |
role_id | string (UUID) or absent | The role granted. Absent means attached, granting nothing |
role_name | string | Joined for display |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
One grant row carries one role, so a group holding two roles on a workspace has two rows.
Group Members
Manage who is in a group. Adding somebody to a group is what gives them access to every workspace that group reaches.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/organizations/{orgId}/groups/{groupId}/members | List group members |
POST | /api/v1/organizations/{orgId}/groups/{groupId}/members | Add a member |
DELETE | /api/v1/organizations/{orgId}/groups/{groupId}/members/{accountId} | Remove a member |
GET | /api/v1/workspaces/{id}/members | List everyone who can reach this workspace, with the groups and roles that let them (derived, read-only) |
Add Member
POST /api/v1/organizations/{orgId}/groups/{groupId}/members
{
"account_id": "770e8400-e29b-41d4-a716-446655440000"
}Removing the last member of the Organization admins group is refused — it would leave the organization with nobody able to administer it. Add the replacement administrator first.
Group Member Object
| Field | Type | Description |
|---|---|---|
group_id | string (UUID) | Group ID |
account_id | string (UUID) | Account ID |
email | string | Member email |
name | string | Member name |
avatar_url | string | Member avatar URL |
created_at | string (ISO 8601) | When the member was added |
Destination Policies
Destination policies are sync-time governance policies that restrict which records can be sent to specific destination types. They act as guardrails to prevent sensitive data from reaching certain platforms. The API path remains destination-rules for backwards compatibility.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/workspaces/{id}/destination-rules | List all destination policies |
POST | /api/v1/workspaces/{id}/destination-rules | Create a destination policy |
GET | /api/v1/workspaces/{id}/destination-rules/{ruleId} | Get a destination policy |
PUT | /api/v1/workspaces/{id}/destination-rules/{ruleId} | Update a destination policy |
DELETE | /api/v1/workspaces/{id}/destination-rules/{ruleId} | Delete a destination policy |
GET | /api/v1/workspaces/{id}/destination-types/{destType}/rules | List destination policies by destination type |
GET | /api/v1/workspaces/{id}/destination-rules/{ruleId}/groups | List the groups that may manage this policy |
PUT | /api/v1/workspaces/{id}/destination-rules/{ruleId}/groups | Set the groups that may manage this policy |
Create Destination Policy
POST /api/v1/workspaces/{id}/destination-rules
{
"name": "GDPR - No EU Customers to Facebook",
"description": "Prevent EU customer data from being synced to Facebook Ads",
"parent_model_id": "770e8400-e29b-41d4-a716-446655440000",
"destination_type": "facebook_ads",
"filter_tree": {
"type": "condition",
"condition_type": "property",
"column": "country",
"operator": "not_in",
"value": ["DE", "FR", "IT", "ES", "NL", "BE", "AT", "SE", "DK", "FI"]
},
"enabled": true
}Destination Policy Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
workspace_id | string (UUID) | Owning workspace |
parent_model_id | string (UUID) | Model the policy applies to |
destination_type | string | Destination type this policy restricts |
name | string | Display name |
description | string | Description |
filter_tree | object | Filter criteria (same format as audience filters) |
enabled | boolean | Whether the policy is active |
created_by | string (UUID) | Account that created the policy |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Who May Manage a Policy
A destination policy can name the groups allowed to edit and delete it. This governs editing the policy, not who the policy applies to — a policy is evaluated at sync time, where no user is present, so it always applies to every sync of its destination type.
PUT /api/v1/workspaces/{id}/destination-rules/{ruleId}/groups
{
"group_ids": ["990e8400-e29b-41d4-a716-446655440000"]
}The body states the complete set, so sending {"group_ids": []} clears every claim.
- A policy naming no group can be managed by anyone with
destination_rules.write. Absence is permissive, so nothing changes for policies that predate this. - Once a policy names groups, only
destination_rules.writeholders who belong to one of them may change it — plus workspace owners and admins, so a policy cannot be stranded by deleting the group that claimed it. - Re-claiming is behind the same check, so a claimed policy cannot be taken over by rewriting its claims.
Access Policies
Access Policies provide row-level access control by restricting which records a user or group can see. They use filter trees (same as audiences) to define visibility boundaries. The API path remains subsets for backwards compatibility.
Access Policy Categories
Categories organize access policies into logical groups (e.g., Region, Brand, Business Unit).
| Method | Path | Description |
|---|---|---|
GET | /api/v1/workspaces/{id}/subset-categories | List categories |
GET | /api/v1/workspaces/{id}/subset-categories/{catId} | Get a category |
POST | /api/v1/workspaces/{id}/subset-categories | Create a category |
PUT | /api/v1/workspaces/{id}/subset-categories/{catId} | Update a category |
DELETE | /api/v1/workspaces/{id}/subset-categories/{catId} | Delete a category |
Access Policy Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/workspaces/{id}/subsets | List all access policies |
GET | /api/v1/workspaces/{id}/subsets/me | List access policies that apply to the caller |
POST | /api/v1/workspaces/{id}/subsets | Create an access policy |
GET | /api/v1/workspaces/{id}/subsets/{subsetId} | Get an access policy |
PUT | /api/v1/workspaces/{id}/subsets/{subsetId} | Update an access policy |
DELETE | /api/v1/workspaces/{id}/subsets/{subsetId} | Delete an access policy |
Create Access Policy
{
"name": "US Region",
"description": "Only US customer records",
"category_id": "550e8400-e29b-41d4-a716-446655440000",
"parent_model_id": "770e8400-e29b-41d4-a716-446655440000",
"filter_tree": {
"type": "condition",
"condition_type": "property",
"column": "country",
"operator": "equals",
"value": "US"
}
}Access Policy Assignments
Assign access policies to groups to restrict their data visibility. Assignments are nested under each access policy.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/workspaces/{id}/subsets/{subsetId}/assignments | List assignments for an access policy |
POST | /api/v1/workspaces/{id}/subsets/{subsetId}/assignments | Create an assignment |
DELETE | /api/v1/workspaces/{id}/subsets/{subsetId}/assignments/{assignmentId} | Remove an assignment |
GET | /api/v1/workspaces/{id}/groups/{groupId}/subsets | List access policies assigned to a group |
PUT | /api/v1/workspaces/{id}/groups/{groupId}/subsets | Replace the set of access policies assigned to a group |
Create Assignment
{
"group_id": "aae8400-e29b-41d4-a716-446655440000"
}Access Policy Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
category_id | string (UUID) | Parent category |
workspace_id | string (UUID) | Owning workspace |
parent_model_id | string (UUID) or null | Model the access policy applies to |
name | string | Display name |
description | string | Description |
filter_tree | object | Filter criteria |
created_by | string (UUID) | Account that created the access policy |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Folder and Destination Access Lists
Restrict a folder or a destination to named groups. A resource with no groups is reachable by everyone holding the ordinary read permission. A restricted folder also restricts every folder below it and the audiences, orchestrations and A/B tests filed in them; a restricted destination also restricts the syncs that send to it. Someone with no access gets 404; someone with view-only access who tries to change something gets 403. Editing an access list requires the resource’s own write permission (folders.write or destinations.write). See Folder and Destination Access.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/workspaces/{id}/folders/{folderId}/groups | Get a folder’s access list |
PUT | /api/v1/workspaces/{id}/folders/{folderId}/groups | Replace a folder’s access list |
GET | /api/v1/workspaces/{id}/folder-groups | List every restricted folder in the workspace |
GET | /api/v1/workspaces/{id}/destinations/{destId}/groups | Get a destination’s access list |
PUT | /api/v1/workspaces/{id}/destinations/{destId}/groups | Replace a destination’s access list |
GET | /api/v1/workspaces/{id}/destination-groups | List every restricted destination in the workspace |
Set Access List
A PUT states the complete list; an empty groups removes the restriction. Naming a group already on the list changes its level.
{
"groups": [
{ "group_id": "aae8400-e29b-41d4-a716-446655440000", "access_level": "edit" },
{ "group_id": "bbe8400-e29b-41d4-a716-446655440000", "access_level": "view" }
]
}| Field | Type | Description |
|---|---|---|
groups | object[] | { group_id, access_level } per group. access_level is edit (see and change; for a destination, also send to it) or view (see only). Each group must reach this workspace. Any other level is rejected with 400. |
group_ids | string[] | The older form: every group at edit. Ignored when groups is present. |
Access List Object
| Field | Type | Description |
|---|---|---|
groups | array | { group_id, group_name, access_level } for each named group; empty when unrestricted |
The workspace-wide reads return an array of { resource_id, name, groups }, one per restricted resource. They require the write permission and include resources restricted to groups the caller is not in, so a restriction can always be lifted.
Deletion Rules
Deletion rules erase warehouse data for an audience-scoped population, on demand or on a schedule, and record every run in an audit log. See Deletion Rules for the eligibility rules and safety controls.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/workspaces/{id}/deletion-rules | List all deletion rules |
POST | /api/v1/workspaces/{id}/deletion-rules | Create a deletion rule |
POST | /api/v1/workspaces/{id}/deletion-rules/impact | Resolve what else reads a candidate target list |
POST | /api/v1/workspaces/{id}/deletion-rules/estimate | Count the rows each candidate target would lose |
GET | /api/v1/workspaces/{id}/deletion-rules/{ruleId} | Get a deletion rule |
PUT | /api/v1/workspaces/{id}/deletion-rules/{ruleId} | Update a deletion rule |
DELETE | /api/v1/workspaces/{id}/deletion-rules/{ruleId} | Delete a deletion rule (its log is retained) |
POST | /api/v1/workspaces/{id}/deletion-rules/{ruleId}/dry-run | Start a non-destructive run |
POST | /api/v1/workspaces/{id}/deletion-rules/{ruleId}/run | Start a live run |
GET | /api/v1/workspaces/{id}/deletion-rules/{ruleId}/runs | List a rule’s runs |
GET | /api/v1/workspaces/{id}/deletion-runs | List the workspace deletion log |
GET | /api/v1/workspaces/{id}/deletion-runs/{runId} | Get one run with per-target detail |
Reads require deletion_rules.read, writes deletion_rules.write, and both run endpoints deletion_rules.execute. The estimate endpoint requires deletion_rules.write: it compiles the population, writes a scratch table into the planner schema and reads every target table, so it is part of authoring a rule rather than reading one.
Every endpoint additionally requires the workspace to have the Deletion Rules feature enabled by a platform administrator; otherwise they answer 403 deletion rules are not enabled for this workspace. See Availability.
Eligibility and safety refusals — an ineligible target, an empty filter, a column that cannot be cleared, a pinned join that has since drifted — answer 400 with the specific reason in error. Read it: it names the target and says what is wrong with it, which is the only place that information exists.
Create Deletion Rule
POST /api/v1/workspaces/{id}/deletion-rules
filter_tree is required and must carry at least one condition — a rule may not match every record by default. dry_run defaults to true and enabled to false, so omitting them creates an observe-only rule.
{
"parent_model_id": "770e8400-e29b-41d4-a716-446655440000",
"name": "Erase opted-out EU customers",
"description": "Fulfils standing right-to-erasure requests for EU opt-outs.",
"filter_tree": {
"type": "and",
"children": [
{
"type": "condition",
"condition_type": "property",
"field": "region",
"operator": "equals",
"value": "EU"
},
{
"type": "condition",
"condition_type": "property",
"field": "erasure_requested",
"operator": "equals",
"value": true
}
]
},
"targets": [
{ "model_id": "770e8400-e29b-41d4-a716-446655440000", "action": "delete_rows" },
{
"model_id": "881e8400-e29b-41d4-a716-446655440000",
"action": "null_columns",
"columns": ["shipping_address", "phone"]
}
],
"schedule": "0 3 * * *",
"dry_run": true,
"enabled": false
}Estimate Rows Affected
POST /api/v1/workspaces/{id}/deletion-rules/estimate
Counts how many rows each target would delete or clear right now, for a candidate definition that need not be saved. Nothing is mutated.
The count is not an approximation: it materializes the matched population exactly as a run does and issues the same per-target COUNT the run records as records_affected, so what you preview is what a run reports. It is never cached — call it again to re-measure.
{
"parent_model_id": "770e8400-e29b-41d4-a716-446655440000",
"filter_tree": { "type": "and", "children": [ /* … */ ] },
"targets": [
{ "model_id": "770e8400-e29b-41d4-a716-446655440000", "action": "delete_rows" },
{
"model_id": "881e8400-e29b-41d4-a716-446655440000",
"action": "null_columns",
"columns": ["shipping_address"]
}
]
}{
"matched_count": 1482,
"targets": [
{
"model_id": "770e8400-e29b-41d4-a716-446655440000",
"model_name": "Customers",
"physical_table": "\"ANALYTICS\".\"PUBLIC\".\"CUSTOMERS\"",
"action": "delete_rows",
"records_affected": 1482
},
{
"model_id": "881e8400-e29b-41d4-a716-446655440000",
"model_name": "Orders",
"physical_table": "\"ANALYTICS\".\"PUBLIC\".\"ORDERS\"",
"action": "null_columns",
"columns": ["SHIPPING_ADDRESS"],
"records_affected": 5310
}
]
}matched_count is the population — the number the audience estimate reports for the same filter. Each target’s records_affected is the rows that model loses, which is a different number: one matched customer can own several orders.
A target whose count fails carries an error string and the others still report, mirroring how a run records a per-target failure and carries on. Treat such a target as unknown, not as zero.
A null_columns count only includes rows where at least one selected column still holds a value — the same predicate the UPDATE uses. Counting again after a live run therefore reports 0, which is the property that makes re-running a rule a no-op.
Deletion Rule Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
workspace_id | string (UUID) | Owning workspace |
parent_model_id | string (UUID) | Model defining the entity the rule erases |
name | string | Display name (unique per workspace) |
description | string | Description |
filter_tree | object | Match criteria (same format as audience filters) |
targets | array | Target selections — see below |
schedule | string | Cron expression, optionally CRON_TZ=-prefixed. Empty means manual only |
enabled | boolean | Required for any live deletion |
dry_run | boolean | Observe mode: every run reports and mutates nothing |
last_run_at | string (ISO 8601) or null | When the most recent run started |
last_run_status | string | Status of the most recent run |
created_by | string (UUID) | Account that created the rule |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Deletion Target Object
| Field | Type | Description |
|---|---|---|
model_id | string (UUID) | Target model. Must be the parent model or directly related to it, on the same warehouse, and backed by a warehouse table |
action | string | delete_rows (physical DELETE) or null_columns (UPDATE … = NULL) |
columns | array of string | Required for null_columns. May not include the join key to the parent model or the target’s primary key |
Trigger a Run
POST /api/v1/workspaces/{id}/deletion-rules/{ruleId}/run
POST /api/v1/workspaces/{id}/deletion-rules/{ruleId}/dry-run
Both return 202 Accepted with the opened run in running state — the deletion continues past the request, so poll the run log to follow it. /dry-run never mutates, whatever the rule’s own dry_run value. /run returns 400 when the rule is disabled.
Deletion Run Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
rule_id | string (UUID) or null | Source rule. null once the rule has been deleted |
workspace_id | string (UUID) | Owning workspace |
rule_name | string | Rule name snapshotted at run time |
rule_snapshot | object | Parent model, filter tree, targets and schedule as they were at run time |
trigger_type | string | manual or schedule |
mode | string | dry_run or execute |
status | string | running, completed, partial (some targets failed), or failed |
matched_count | integer | Records matched by the filter |
records_affected | integer | Records affected across all targets. In a dry run, records that would be |
error_message | string | Run-level failure detail |
initiated_by | string (UUID) or null | Account that started the run |
initiated_by_email | string | Email snapshotted at run time, so the log stays attributable |
started_at | string (ISO 8601) | Start timestamp |
completed_at | string (ISO 8601) or null | Completion timestamp |
targets | array | Per-target outcomes — see below |
Deletion Run Target Object
| Field | Type | Description |
|---|---|---|
model_name | string | Target model name at run time |
physical_table | string | Fully qualified warehouse table |
action | string | delete_rows or null_columns |
target_columns | array of string | Physical columns cleared |
records_affected | integer | Records affected. 0 on failure |
status | string | completed, failed, or skipped |
error_message | string | Failure detail for this target |
statement | string | The statement executed — or, in a dry run, the statement that would be |
executed_at | string (ISO 8601) | When this target was processed |