Documentation
¶
Overview ¶
Package api is a thin HTTP client for Ronja's /api/v2 surface.
It is deliberately hand-rolled rather than generated from the OpenAPI spec: the CLI touches a handful of endpoints, and a generated client would drag in a code-generation step and a dependency for no benefit at this size.
Index ¶
- Constants
- Variables
- func CodeOf(err error) string
- func DescribeLifecycle(lifecycle string) string
- func IsTimeout(err error) bool
- func KitDocsPath(kitPath string) string
- func SameAccess(a, b DataAppAccess) bool
- func StatusOf(err error) int
- type BusyError
- type Client
- func (c *Client) ApplyMigrations(ctx context.Context, databaseID string, in MigrationSetInput, ...) (*MigrationSetResult, error)
- func (c *Client) CheckoutDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
- func (c *Client) CheckoutTable(ctx context.Context, id string) (*Table, error)
- func (c *Client) CheckoutWorkflow(ctx context.Context, id string) (*Workflow, error)
- func (c *Client) CommitDataAppDraft(ctx context.Context, draftID string) (*DataAppCommitOutcome, error)
- func (c *Client) CommitTableDraft(ctx context.Context, draftID string) error
- func (c *Client) CommitWorkflowDraft(ctx context.Context, draftID, confirmHeadVersionID string) error
- func (c *Client) CreateDataApp(ctx context.Context, in CreateDataAppInput) (*DataApp, error)
- func (c *Client) CreateTable(ctx context.Context, in CreateTableInput) (*Table, error)
- func (c *Client) CreateWorkflow(ctx context.Context, in CreateWorkflowInput) (*Workflow, error)
- func (c *Client) DatabaseSQL(ctx context.Context, databaseID string, in DatabaseSQLInput, ...) (*DatabaseSQLResult, error)
- func (c *Client) DeleteDataApp(ctx context.Context, id string) error
- func (c *Client) DeleteDataAppFile(ctx context.Context, id, path string) (*DataAppFileDeleteResponse, error)
- func (c *Client) DeleteWorkflow(ctx context.Context, id string) error
- func (c *Client) DeleteWorkflowFile(ctx context.Context, id, path string, baseSha256 *string) (*FileDeleteResponse, error)
- func (c *Client) DiscardDataAppDraft(ctx context.Context, draftID string) error
- func (c *Client) DiscardTableDraft(ctx context.Context, draftID string) error
- func (c *Client) DiscardWorkflowDraft(ctx context.Context, draftID string) error
- func (c *Client) Do(ctx context.Context, method, path string, body, out any) error
- func (c *Client) DoRaw(ctx context.Context, method, path string, header http.Header, body []byte, ...) (*RawResponse, error)
- func (c *Client) DownloadFileByID(ctx context.Context, fileID string) (io.ReadCloser, error)
- func (c *Client) DownloadFrame(ctx context.Context, shot PreviewScreenshot) (io.ReadCloser, error)
- func (c *Client) GetDataApp(ctx context.Context, id string) (*DataApp, error)
- func (c *Client) GetDataAppDraft(ctx context.Context, id string) (*DataApp, error)
- func (c *Client) GetDataAppFile(ctx context.Context, id, path string) (*DataAppFile, error)
- func (c *Client) GetFeature(ctx context.Context, id string) (*Feature, error)
- func (c *Client) GetFile(ctx context.Context, id string) (*File, error)
- func (c *Client) GetKitFile(ctx context.Context, kitPath string) (string, error)
- func (c *Client) GetTable(ctx context.Context, id string) (*Table, error)
- func (c *Client) GetTableDraft(ctx context.Context, id string) (*Table, error)
- func (c *Client) GetTableDraftReview(ctx context.Context, draftID string) (*TableDraftReview, error)
- func (c *Client) GetText(ctx context.Context, path string) (string, error)
- func (c *Client) GetWorkflow(ctx context.Context, id string) (*Workflow, error)
- func (c *Client) GetWorkflowDraft(ctx context.Context, id string) (*Workflow, error)
- func (c *Client) GetWorkflowFile(ctx context.Context, id, path string) (*WorkflowFile, error)
- func (c *Client) GetWorkflowRun(ctx context.Context, runID string) (*RunResponse, error)
- func (c *Client) HeadVersionID(ctx context.Context, parentID string) (string, error)
- func (c *Client) ListDataAppFiles(ctx context.Context, id string) ([]DataAppFile, error)
- func (c *Client) ListFeatureTables(ctx context.Context, featureID string) ([]*TableListItem, error)
- func (c *Client) ListWorkflowFiles(ctx context.Context, id string) ([]WorkflowFile, error)
- func (c *Client) ListWorkflowRuns(ctx context.Context, workflowID string, limit int) ([]WorkflowRun, error)
- func (c *Client) ListWorkflowVersions(ctx context.Context, parentID string) ([]Workflow, error)
- func (c *Client) Me(ctx context.Context) (*Me, error)
- func (c *Client) PollDevice(ctx context.Context, start *DeviceStart) (*DeviceToken, error)
- func (c *Client) PreviewDataApp(ctx context.Context, appID string, in PreviewRequest) (*PreviewOutcome, error)
- func (c *Client) PublishWorkflowDraft(ctx context.Context, draftID string) error
- func (c *Client) PutDataAppFile(ctx context.Context, id, path, content string) (*DataAppFileSaveResponse, error)
- func (c *Client) PutWorkflowFile(ctx context.Context, id, path, content string, baseSha256 *string) (*FileSaveResponse, error)
- func (c *Client) Query(ctx context.Context, in QueryInput, timeout time.Duration) (*QueryResult, error)
- func (c *Client) RequestDataAppReview(ctx context.Context, draftID string) (*DataAppCommitOutcome, error)
- func (c *Client) RequestTableReview(ctx context.Context, draftID string) error
- func (c *Client) RequestWorkflowReview(ctx context.Context, draftID string) error
- func (c *Client) ResumeWorkflowRun(ctx context.Context, workflowID, resumeOfRunID string) (*WorkflowRun, error)
- func (c *Client) RunWorkflow(ctx context.Context, workflowID string, parameterValues map[string]any) (*WorkflowRun, error)
- func (c *Client) StartDevice(ctx context.Context, clientName string) (*DeviceStart, error)
- func (c *Client) SyncTable(ctx context.Context, id string) error
- func (c *Client) UpdateDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
- func (c *Client) UpdateTableCode(ctx context.Context, id, code string, inputModels []string) error
- func (c *Client) UpdateWorkflow(ctx context.Context, id string, patch WorkflowPatch) error
- func (c *Client) ValidateDataApp(ctx context.Context, draftID string) (*DataApp, error)
- func (c *Client) ValidateWorkflowFiles(ctx context.Context, in ValidateInput) (*ValidateResult, error)
- func (c *Client) WaitForTableBuild(ctx context.Context, id string) (*TableBuildResult, error)
- type CommitDraftInput
- type CompileDiagnostic
- type CompileError
- type CreateDataAppInput
- type CreateTableInput
- type CreateWorkflowInput
- type DataApp
- type DataAppAccess
- type DataAppAudience
- type DataAppCommitOutcome
- type DataAppFile
- type DataAppFileDeleteResponse
- type DataAppFileSaveResponse
- type DataAppPatch
- type DatabaseSQLInput
- type DatabaseSQLResult
- type DeviceStart
- type DeviceToken
- type Error
- type Feature
- type File
- type FileDeleteResponse
- type FileSaveResponse
- type Me
- type Migration
- type MigrationResult
- type MigrationSetInput
- type MigrationSetResult
- type PreviewBusy
- type PreviewConsoleError
- type PreviewHarnessRequest
- type PreviewNetworkFailure
- type PreviewOp
- type PreviewOutcome
- type PreviewRequest
- type PreviewResult
- type PreviewRuntimeError
- type PreviewScreenshot
- type PreviewSettle
- type PreviewStep
- type PreviewStepResult
- type PreviewStepSettle
- type QueryInput
- type QueryResult
- type RawResponse
- type RunResponse
- type StepDTO
- type Table
- type TableBuildError
- type TableBuildPoll
- type TableBuildResult
- type TableDraftReview
- type TableFieldDelta
- type TableInputModelsDelta
- type TableInterveningVersion
- type TableListItem
- type UpdateTableInput
- type ValidateBindings
- type ValidateFile
- type ValidateFinding
- type ValidateInput
- type ValidateResult
- type Workflow
- type WorkflowApprovalGate
- type WorkflowFile
- type WorkflowParameter
- type WorkflowPatch
- type WorkflowRun
- type WorkflowRunOutput
- type WorkflowRunTableOutput
Constants ¶
const ( ErrAuthorizationPending = "authorization_pending" ErrSlowDown = "slow_down" ErrExpiredToken = "expired_token" ErrAccessDenied = "access_denied" ErrInvalidGrant = "invalid_grant" )
The device-authorization flow. Mirrors backend/api/v2/authentication/ api_cli_auth.go — the poll error codes are a wire contract, so they are matched by exact string.
const ( DefaultMinPollInterval = 5 * time.Second DefaultPollBackoff = 2 * time.Second DefaultMaxPollInterval = 20 * time.Second // DefaultMaxPollFailureWindow bounds an UNBROKEN RUN of transient poll // failures (a dropped connection, a 429, a 5xx from a pod restarting // mid-deploy) by ELAPSED TIME rather than by a count. // // A count was the wrong unit: it silently means "count x interval", so the // real tolerance moved whenever the cadence did. Five retries at the 5s // floor is ~25 seconds, which is less than an ordinary rolling deploy takes // — a login would die on a perfectly healthy instance mid-release. Elapsed // time says what is actually meant, and stays honest under backoff. // // Generous is nearly free here: a login has ~10 minutes of life, and the // flow deadline bounds total wall time independently of this. What this // still rules out is the case it exists for — a permanently broken instance // hidden behind a silent loop for the full ten minutes. DefaultMaxPollFailureWindow = 90 * time.Second // DefaultRateLimitBackoff is added to the interval on a 429, which is the // global rate limiter answering. It is the one transient failure where // retrying at the same cadence is actively wrong: the server is saying // there is too much traffic, and an unchanged cadence is a promise to keep // producing exactly as much. DefaultRateLimitBackoff = 5 * time.Second )
Default device-flow poll timing. The server advertises its own interval and enforces it, so these are a floor, a backoff step and a ceiling rather than the primary cadence.
const ( // MigrationApplied — the DDL ran and a ledger row was written. MigrationApplied = "applied" // MigrationSkipped — already in the ledger with identical SQL. MigrationSkipped = "skipped" // MigrationPending — not in the ledger; a real apply would apply it. MigrationPending = "pending" // MigrationDrifted — in the ledger under DIFFERENT SQL. A real apply // refuses the whole set. MigrationDrifted = "drifted" )
Migration outcomes, as reported per migration. The first two are what a real apply returns; the last two only ever come back from a dry run.
const ( TableKindDerived = "derived" TableKindMetric = "metric" )
Table kinds, mirrored from rdb.ModelKind (backend/ronja/rdb/table_model_v2.go). Only the two the pipeline loop reasons about are named: `derived` is what it manages, and `metric` is the other DRAFTABLE kind — it runs the identical checkout/sync/commit flow and is out of scope by choice, not by capability, so a message about it must never claim otherwise. The remaining kinds (foundation, integration, manual_integration, dynamic) are reported by their raw string; naming them here would be four constants nothing branches on.
const ( TableStatusPending = "pending" TableStatusInvalidated = "invalidated" TableStatusBuilding = "building" TableStatusReady = "ready" TableStatusBuildFailed = "build_failed" )
Table build statuses, mirrored from rdb.ModelStatus.
The CLI reads BuildVerdict, not Status — see WaitForTableBuild. These exist only for the fallback that derives a verdict from an older server's response, which is the one place the raw status is still branched on.
const ( ShadowStatusDraft = "draft" ShadowStatusCommitted = "committed" )
Draft lifecycle, mirrored from rdb.ShadowStatus. A row with no shadow status is a plain live table.
const ( // BuildVerdictOK — the last run succeeded. BuildVerdictOK = "ok" // BuildVerdictOKPartial — queryable, but the build skipped corrupt source // files, so the data is incomplete. BuildVerdictOKPartial = "ok_partial" // BuildVerdictFailedStale — the last run FAILED and the table is still // serving the PREVIOUS build's data. `status` reads `ready` here. BuildVerdictFailedStale = "failed_stale" // BuildVerdictFailed — the run failed with no earlier data to fall back on. BuildVerdictFailed = "failed" // BuildVerdictBuilding — still running; keep polling. BuildVerdictBuilding = "building" // BuildVerdictPending — queued and not started. A sync of a table with no // `code` parks here forever, so a `pending` that never advances is a // failure, not progress. BuildVerdictPending = "pending" // BuildVerdictInvalidated — an upstream table changed; this one's data is // stale pending a rebuild. BuildVerdictInvalidated = "invalidated" )
Build verdicts, mirrored from the buildVerdict* consts in backend/api/v2/feature/tableview.go.
This is the field to branch on after a sync, and the reason it exists: a live table whose re-sync FAILS stays `ready` and goes on serving the previous build's data, so `status` alone reports a failure as success. The CLI branches on these by exact string, which makes them a wire contract in the same sense the device-flow poll codes are.
const ( DefaultTableBuildPollInterval = 2500 * time.Millisecond DefaultMaxTableBuildPollInterval = 10 * time.Second DefaultTableBuildTimeout = 15 * time.Minute // DefaultMaxTableBuildFailureWindow is the same 90 seconds the device flow // tolerates, and for the same reason: a rolling deploy takes longer than any // small number of polls, and the overall Timeout is the independent backstop. DefaultMaxTableBuildFailureWindow = 90 * time.Second )
The default poll schedule.
The interval matches `wf test`'s (the web builder polls at 3s; 2.5s is the same order), and for the same reason it holds that cadence for the first few polls before backing off: most syncs of a small table finish in seconds, and backing off immediately would make the common case worse to protect the rare one.
The timeout is generous because a derived build over a large partitioned input legitimately takes minutes, and giving up early on a healthy build reports a working instance as a broken one.
const ( LifecycleLive = "live" LifecycleDraft = "draft" LifecycleVersion = "version" LifecycleArchived = "archived" LifecycleProposed = "proposed" )
Workflow lifecycle values, mirrored from rdb.WorkflowLifecycle (backend/ronja/rdb/table_workflow.go). The CLI branches on these by exact string, so they are a wire contract in the same sense the device-flow poll codes are.
const ( RunStatusRunning = "running" RunStatusDone = "done" RunStatusError = "error" RunStatusWaiting = "waiting" RunStatusResuming = "resuming" )
Workflow run statuses, mirrored from rdb.WorkflowRunStatus (backend/ronja/rdb/table_workflow.go). The enum is exactly these five, and the poll loop's terminal condition is "not running", so a status added server-side would end the poll rather than hang it.
The last two belong to durable waits and only ever appear on a runtime-2 run that suspended:
- waiting is PARKED: the burst finished cleanly, its outputs are persisted, and the server knows what has to happen before the run resumes. Nothing failed and nothing is executing, so it is neither a success nor a failure — it is a run that is not finished yet.
- resuming is the wake race's single-winner latch, between "a wake decided to resume this run" and "the resume run exists". Terminal for THIS row: the parked run never executes again, its successor carries the lineage.
Both are real persisted statuses, so a poll can land on either — briefly on resuming, indefinitely on waiting — which is why the CLI has to name them rather than let them fall through to "a status this version does not know" and report a parked run as a failure.
const ( RunHealthDone = "done" RunHealthDegraded = "degraded" RunHealthFailed = "failed" RunHealthWaiting = "waiting" )
Derived run health, mirrored from workflow.RunHealth (backend/api/v2/workflow/run_steps.go). Never stored: the server computes it on read from the run status plus the step rows. Degraded is the one worth knowing about — a run that finished but had a best-effort step fail, which a bare "done" would hide. Waiting is the parked run's health, and it is a SEPARATE value rather than a flavour of done for the reason the status is: a parked run reported as healthy tells the reader a stalled pipeline succeeded.
const DefaultDatabaseSQLTimeout = slowRequestTimeout
DefaultDatabaseSQLTimeout bounds one `db sql` call when the caller names no deadline.
The slow bound, matching Query: a statement against a real Postgres database is real work, and an aggregate over a large table legitimately outlasts a read timeout. It remains only a DEFAULT — --timeout moves it, and a value past the client's own ceiling is honoured (see httpFor).
const DefaultMigrationTimeout = 20 * time.Minute
DefaultMigrationTimeout bounds one migration set.
Deliberately far longer than a query default. The server runs the whole set in ONE transaction holding a per-database advisory lock, under a 20-minute ceiling of its own; a client that gave up sooner would abandon a migration that is still committing and leave the operator with no idea whether it landed. Better to wait for the answer than to guess at it.
const DefaultQueryTimeout = slowRequestTimeout
DefaultQueryTimeout bounds one query when the caller names no deadline.
The slow bound rather than the read bound, for the usual reason — a query is real work server-side. It is only a DEFAULT, though: a large query legitimately routes to Batch compute and takes minutes, so `ronja query --timeout` can move it, and Query honours a value past the client's own ceiling. See httpFor.
const DefaultRawTimeout = slowRequestTimeout
DefaultRawTimeout bounds one raw call when the caller names no deadline.
The slow bound rather than the read bound: an arbitrary POST can be anything, including the endpoints that legitimately do real work server-side, and killing one of those at 30s reports a healthy instance as a broken one.
It is only a DEFAULT, for the same reason DefaultQueryTimeout is: `ronja api` can be pointed at an endpoint that runs for minutes, and a bound nobody can move is a bound that eventually kills a healthy request. `--timeout` moves it, including past the client's own ceiling — see httpFor.
const KitIndexPath = "/docs/api/kit.md"
KitIndexPath is the index listing every kit file with a one-line description.
const PartialDataCorruptErrorType = "partial_data_corrupt"
PartialDataCorruptErrorType is the rjerr type string (rjerr.ERR_PARTIAL_DATA_CORRUPT) that marks the NON-FATAL "the build completed but skipped corrupt source files" notice. Anything else on a `ready` row is a real failure. Used only by the verdict fallback below.
const QueryFormatCSVMeta = "csv_meta"
QueryFormatCSVMeta asks for the envelope the CLI is built around: a CSV string plus the row count, the truncation flag and the failure field.
The CLI always sends it. The endpoint has other formats, and picking between them would be a flag nobody can answer — CSV is the one shape that is both human-legible at a terminal and directly consumable by every data tool, and the "meta" half is what makes truncation and a failed query distinguishable from an empty result.
const SeverityError = "error"
SeverityError is mirrored from rworkflow's SeverityError. The CLI branches on it by exact string — an error finding is what makes `wf push` refuse — so it is a wire contract.
The server's finding CODES (invalid_path, unresolved_ref, secret_dropped, …) are deliberately not mirrored as constants. The CLI never branches on a code: it prints Message and Path, so an unknown code is displayed as faithfully as a known one, and a mirrored list would only be one more thing to keep in step with backend/resource/rworkflow/validate_files.go.
const StatusConflict = http.StatusConflict
StatusConflict is the status both optimistic-concurrency refusals answer with — a file whose stored content is not what the write asserted, and a commit whose parent has moved since the draft was seeded.
An alias for net/http's constant rather than a second spelling of 409: named because the CLI branches on it in two commands and the branch is load-bearing — a 409 must never be routed into `wf publish`'s needs-review fallback, which is keyed on 400 — but the VALUE has one definition.
const TableEngineDuckDB = "duckdb"
TableEngineDuckDB is the only engine value, mirrored from rdb.ModelEngineDuckDB. It means `code` is pure DuckDB SQL with `{{ ref('table-…') }}` markers, which is exactly what a pipeline folder's .sql files are — so it is sent explicitly at create rather than left to the server's default. The default only applies when `code` is present, and stating it costs one field and removes a conditional nobody would remember.
const UserAgent = "ronja-cli"
UserAgent identifies the CLI in server logs.
Variables ¶
var ( ErrDeviceDenied = errors.New("the request was declined in the browser") ErrDeviceExpired = errors.New("the code expired before it was approved") )
ErrDeviceDenied and ErrDeviceExpired are the terminal outcomes a caller is expected to explain to a human, as opposed to genuine failures.
Functions ¶
func CodeOf ¶
CodeOf extracts the server's machine-readable error code, or "" if the error was not an HTTP error.
func DescribeLifecycle ¶
DescribeLifecycle renders a lifecycle value for a human, including the ones the CLI refuses to work with.
func IsTimeout ¶
IsTimeout reports a request that died on a DEADLINE rather than on an answer.
The distinction is what makes a write recoverable: a 4xx means the server considered the request and refused it, while a timeout means we stopped listening — the write may well have committed, and the caller has to go and look rather than assume either way.
Both forms are checked because they arrive differently: a per-request context deadline surfaces as context.DeadlineExceeded, while the http.Client's own Timeout surfaces as a *url.Error that merely reports Timeout() — and which of the two fired is not something a caller should have to reason about.
func KitDocsPath ¶ added in v0.27.0
KitDocsPath is the docs route serving one kit file, by its kit-relative path ("components/ui/button.tsx").
func SameAccess ¶
func SameAccess(a, b DataAppAccess) bool
SameAccess compares two access sets for sync purposes, order-insensitively.
Types ¶
type BusyError ¶ added in v0.27.0
type BusyError struct {
Message string
Calibration string
// RetryAfter is the parsed Retry-After header, or 0 when the server sent
// none (or something unparseable). The caller supplies its own default.
RetryAfter time.Duration
// Body is the 429 verbatim, so a --json caller can be shown what the server
// actually said rather than this struct's reading of it.
Body []byte
}
BusyError is a 429 from the preview endpoint, surfaced as its own type rather than as a plain *Error with StatusOf(err) == 429.
Two reasons it earns a type. It is not a failure of the REQUEST — the app was never rendered, so nothing at all is known about it, and a caller must not report it as "your app is broken" or change any code in response. And it is the one answer that names its own remedy: the Retry-After the server sent, which a retry that ignored it would be a rate limiter's definition of abuse.
type Client ¶
type Client struct {
BaseURL string
Token string
HTTP *http.Client
// MinPollInterval floors the cadence PollDevice uses, so a server that
// advertises nothing (or zero) still gets polled politely.
MinPollInterval time.Duration
// PollBackoff is added to the interval each time the server answers
// slow_down.
PollBackoff time.Duration
// MaxPollInterval caps that backoff, so a run of slow_down answers cannot
// stretch the cadence past the point where the flow expires unpolled.
MaxPollInterval time.Duration
// MaxPollFailureWindow bounds an unbroken run of transient failures by
// elapsed time before PollDevice gives up. See isTransient.
MaxPollFailureWindow time.Duration
// RateLimitBackoff is added to the interval on a 429.
RateLimitBackoff time.Duration
// TableBuild is the schedule WaitForTableBuild polls on. Grouped rather than
// flattened alongside the device-flow knobs above, because "MaxInterval"
// belonging to one loop or the other is not something a reader should have
// to infer from a comment.
TableBuild TableBuildPoll
}
func (*Client) ApplyMigrations ¶
func (c *Client) ApplyMigrations(ctx context.Context, databaseID string, in MigrationSetInput, timeout time.Duration) (*MigrationSetResult, error)
ApplyMigrations applies (or, with DryRun, plans) an ordered migration set.
func (*Client) CheckoutDataApp ¶
func (c *Client) CheckoutDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
CheckoutDataApp forks a live data app into the caller's own draft, optionally overlaying a metadata + allowlist patch in the same call.
Idempotent on the draft: a caller who already has one gets it back. The overlay is what makes a first push one request instead of two, and the server re-checks every bound id in it against the caller's access.
func (*Client) CheckoutTable ¶ added in v0.27.0
CheckoutTable gets-or-creates the CALLER'S OWN draft of a table and returns it. The returned row has a NEW id, and every subsequent call in the edit loop takes that id rather than the original's.
NOT idempotent, unlike CheckoutWorkflow: a second checkout while a draft is already open is an error. GetTableDraft is how an existing draft is picked back up, which is why the push path always tries that first.
The draft copies the original's config and its data references, so it is queryable before anything changes — but a draft that has been checked out and never SYNCED has no partitions of its own, so sample rows are only ever read after a successful sync.
func (*Client) CheckoutWorkflow ¶
CheckoutWorkflow gets-or-creates the caller's own draft of a live workflow. Idempotent: calling it twice returns the same draft.
func (*Client) CommitDataAppDraft ¶
func (c *Client) CommitDataAppDraft(ctx context.Context, draftID string) (*DataAppCommitOutcome, error)
CommitDataAppDraft applies a draft onto its parent — or, for a never-published app the caller may not publish itself, submits it for an admin's approval. Takes the DRAFT id. Read the returned outcome before reporting a publish.
func (*Client) CommitTableDraft ¶ added in v0.27.0
CommitTableDraft publishes a draft onto the table it was forked from. Takes the DRAFT's id — this is the one call in the loop where the wrong id silently targets the wrong row.
Two consequences worth holding on to:
- It OVERWRITES the parent's fields from the draft, and there is no compare-and-swap. A stale draft silently reverts whatever landed in between; the review payload's baseStale + interveningVersions is the only warning that exists, and nothing on this call will stop it.
- It CASCADES. The server emits a materialized event and dependent tables are invalidated and rebuilt asynchronously — which is why the loop needs no `run` verb, and why a publish is worth saying out loud.
Refused on a shared feature for a non-admin, and possibly for an admin committing their own draft under a self-approval policy. Decide which branch to take from the feature's scope and the caller's role BEFORE calling, rather than from the rejection prose.
func (*Client) CommitWorkflowDraft ¶
func (c *Client) CommitWorkflowDraft(ctx context.Context, draftID, confirmHeadVersionID string) error
CommitWorkflowDraft applies a draft onto its parent. Takes the DRAFT id.
confirmHeadVersionID is empty for an ordinary commit, which sends no body at all — the shape this route took before the CAS existed. Supply it only after a 409 has named the version being overwritten, and only on a deliberate override (`wf publish --overwrite-remote`).
func (*Client) CreateDataApp ¶
CreateDataApp creates a data app inside a feature. The row it returns is LIVE and empty — see CreateDataAppInput.
func (*Client) CreateTable ¶ added in v0.27.0
CreateTable creates a derived table inside a feature.
The row it returns is LIVE, not a draft — unlike a workflow or a data app, whose create endpoints mint a parentless draft that publish promotes in place. A table created here is visible to the feature immediately, with no data until something syncs it. (Parentless table drafts are a known follow-up; until then a caller that creates a table has created something colleagues can see.)
Admin-only server-side, even though editing a draft is not: create is the one step of the loop a non-admin cannot perform.
func (*Client) CreateWorkflow ¶
CreateWorkflow creates a workflow inside a feature. The row it returns is a parentless DRAFT — hidden until published — which is why the first push needs no separate checkout.
func (*Client) DatabaseSQL ¶
func (c *Client) DatabaseSQL(ctx context.Context, databaseID string, in DatabaseSQLInput, timeout time.Duration) (*DatabaseSQLResult, error)
DatabaseSQL runs one statement against a managed database.
func (*Client) DeleteDataApp ¶
DeleteDataApp marks a data app for deletion. Unlike DiscardDataAppDraft this removes the APP, so it is only ever reached through an explicit opt-in.
The one case the CLI needs it for is a parentless draft — an app created by a first push and then abandoned, typically because that push failed to compile. Discard cannot express it (there is no parent for the row to fall back to), and without this the only remaining answer is the web UI, which is no answer at all for a caller that has nothing but a terminal.
func (*Client) DeleteDataAppFile ¶
func (c *Client) DeleteDataAppFile(ctx context.Context, id, path string) (*DataAppFileDeleteResponse, error)
DeleteDataAppFile removes one file. The server refuses to delete the entrypoint; its message is passed through rather than second-guessed.
func (*Client) DeleteWorkflow ¶
DeleteWorkflow soft-deletes a workflow — the row survives a 30-day TTL before the janitor purges it. Unlike DiscardWorkflowDraft this removes the WORKFLOW, so it is only ever reached through an explicit opt-in.
The one case the CLI needs it for is a parentless draft — a workflow created by a first push and then abandoned. Discard cannot express it (there is no parent for the row to fall back to), and without this the only remaining answer is the web UI, which is no answer at all for a caller that has nothing but a terminal.
func (*Client) DeleteWorkflowFile ¶
func (c *Client) DeleteWorkflowFile(ctx context.Context, id, path string, baseSha256 *string) (*FileDeleteResponse, error)
DeleteWorkflowFile removes one file from a draft. The server refuses to delete the entrypoint; its message is passed through rather than second- guessed, because the remedy (change the entrypoint first) is its to state.
baseSha256 asserts what is being deleted, and nil sends NO BODY AT ALL rather than `{}` — the route took none before preconditions existed, and an unconditional delete has no reason to start sending one.
func (*Client) DiscardDataAppDraft ¶
DiscardDataAppDraft deletes a draft, throwing its uncommitted changes away. The parent live app is untouched.
func (*Client) DiscardTableDraft ¶ added in v0.27.0
DiscardTableDraft deletes a draft, throwing its uncommitted changes away. The parent table is untouched. Takes the DRAFT's id, and is not reversible.
func (*Client) DiscardWorkflowDraft ¶
DiscardWorkflowDraft deletes a draft, throwing its uncommitted changes away. The parent live workflow is untouched.
func (*Client) Do ¶
Do issues a request against /api/v2 and decodes a JSON response into out. Pass a nil body to send none, and a nil out to discard the response.
func (*Client) DoRaw ¶
func (c *Client) DoRaw(ctx context.Context, method, path string, header http.Header, body []byte, timeout time.Duration) (*RawResponse, error)
DoRaw issues an arbitrary request against a path relative to the instance root and returns the response without reading it.
header is applied verbatim; the defaults below fill in only what it does not already carry, so a caller can override any of them — including Authorization, which is how someone tests a different credential without re-logging-in.
timeout bounds the whole call, the response body INCLUDED — the deadline rides on Body.Close, not on this function's return. Zero or less means no client-side deadline at all, which the command surfaces as `--timeout 0`.
func (*Client) DownloadFileByID ¶ added in v0.27.0
DownloadFileByID streams a file's bytes, given its id. The caller owns the returned reader and MUST close it — closing is also what releases the request's deadline.
Two calls, because the download route is keyed on the storage KEY rather than on the file id: the row first, then the bytes.
⚠️ The key is escaped WHOLE, slashes included, into one path segment — the route is `/download/:key`, not a wildcard, and gin unescapes the parameter on the other side. Joining the key's own slashes into the path would produce a URL that matches no route at all.
func (*Client) DownloadFrame ¶ added in v0.27.0
func (c *Client) DownloadFrame(ctx context.Context, shot PreviewScreenshot) (io.ReadCloser, error)
DownloadFrame streams one preview frame's bytes.
It prefers the KEY the preview already handed over — one call, on the same scope the preview itself needs — and falls back to resolving the id only when the instance predates the key (which costs an extra call AND a route a scoped token cannot reach; see PreviewScreenshot.FileKey).
func (*Client) GetDataApp ¶
GetDataApp reads a data app's metadata. 404s for an id the caller cannot reach — the read gate does not distinguish absent from invisible.
func (*Client) GetDataAppDraft ¶
GetDataAppDraft reads the CALLER'S OWN open draft of a data app, returning (nil, nil) when they have none.
The endpoint answers 200 with a JSON `null` body rather than 404 in that case, so decoding into a **DataApp is not a stylistic choice: decoding into a struct would turn "no draft" into a zero-value DataApp with an empty id, and every caller would then act on a draft that does not exist.
func (*Client) GetDataAppFile ¶
GetDataAppFile reads one file back by path, answering 404 when the row holds none.
It lives on the write side because it exists for the write side: a PUT that died on a TIMEOUT may still have committed, and the only way to find out which is to ask. Note that it is subject to the same silent draft resolution as ListDataAppFiles — pass the id you mean.
func (*Client) GetFeature ¶
GetFeature reads a feature's metadata. 404s for an id the caller cannot reach.
func (*Client) GetFile ¶ added in v0.27.0
GetFile reads a file row. 404s for an id the caller cannot reach.
func (*Client) GetKitFile ¶ added in v0.27.0
GetKitFile returns the verbatim source of one component-kit file.
Unauthenticated (GetText attaches no credential), because the docs surface is and because the kit is identical for every tenant — it is code shipped in the binary, not customer data.
A 404 is translated rather than passed on: the instance answers an unknown kit path with the generic /docs/api breadcrumb, which reads as "wrong host" to someone who mistyped a filename. Any kit paths the breadcrumb's near-miss suggestions name are carried into the message, since a one-letter typo is by far the likeliest way to get here.
func (*Client) GetTable ¶
GetTable reads one table row. 404s for an id the caller cannot reach — the read gate does not distinguish absent from invisible.
This is the only endpoint that serves BuildVerdict, LastBuildError and the full `code`, which is why the pipeline loop's status command pays for it per file instead of reading everything off the list.
func (*Client) GetTableDraft ¶ added in v0.27.0
GetTableDraft reads the CALLER'S OWN open draft of a table, returning (nil, nil) when they have none.
The endpoint answers 200 with a JSON `null` body rather than 404 in that case, so decoding into a **Table is not a stylistic choice: decoding into a struct would turn "no draft" into a zero-value Table with an empty id, and every caller would then act on a draft that does not exist. Same shape, and same reasoning, as GetWorkflowDraft.
`id` is the LIVE table's id; the row that comes back has its own, and that is the id the rest of the edit loop takes.
func (*Client) GetTableDraftReview ¶ added in v0.27.0
func (c *Client) GetTableDraftReview(ctx context.Context, draftID string) (*TableDraftReview, error)
GetTableDraftReview reads the diff payload for one DRAFT id. A row that is not an open draft is a 400 (the server's message says so; it is passed through).
func (*Client) GetText ¶
GetText fetches a path relative to the instance ROOT (not /api/v2) and returns it verbatim. The agent-readable docs surface — /llms.txt, /docs/api/endpoints.md, /docs/api/skills.md — lives there and is served unauthenticated, so this deliberately does not require a token.
func (*Client) GetWorkflow ¶
GetWorkflow reads a workflow's metadata. 404s for an id the caller cannot reach — the read gate does not distinguish absent from invisible.
func (*Client) GetWorkflowDraft ¶
GetWorkflowDraft reads the CALLER'S OWN open draft of a workflow, returning (nil, nil) when they have none.
The endpoint answers 200 with a JSON `null` body rather than 404 in that case (see the route's own description in backend/api/v2/workflow/handler.go), so decoding into a **Workflow is not a stylistic choice: decoding into a struct would turn "no draft" into a zero-value Workflow with an empty id, and every caller would then act on a draft that does not exist.
func (*Client) GetWorkflowFile ¶
GetWorkflowFile reads one file back by path, answering 404 when the row holds none.
It lives on the write side because it exists for the write side: a PUT that died on a TIMEOUT may still have committed, and the only way to find out which is to ask. Nothing else reads a single file — the rest of the CLI wants the whole set — so this is a repair tool, not a read API.
func (*Client) GetWorkflowRun ¶
GetWorkflowRun reads one run: the row, its steps and its derived health.
Note the route is /workflow/run/:runID — keyed by the RUN, not nested under the workflow — so polling needs nothing but the id the run started with.
func (*Client) HeadVersionID ¶ added in v0.26.9
HeadVersionID resolves the CAS anchor for a live workflow: the id of its most recently committed version, or — when it has never been versioned — the workflow's OWN id.
The parent-id fallback is not a guess. It mirrors rworkflow.resolveHeadVersionID exactly: an unversioned workflow's drafts are seeded from the live row itself, so the live row IS the anchor, and sending anything else as confirmHeadVersionID would be refused.
Deliberately NOT parsed out of the 409 message. The HTTP error body is the flat app-wide `{"error": "<prose>"}` shape with no structured payload, and a client that regexes an id out of prose starts silently overwriting the wrong version the day somebody rewords it.
func (*Client) ListDataAppFiles ¶
ListDataAppFiles reads every file of ONE data-app row — exactly the row whose id is passed. A draft is its own address: the server never substitutes the caller's open draft for a live id (it once did, silently, which is why every caller in this CLI resolves the draft explicitly with GetDataAppDraft and passes the id it actually means). A baseline is a claim about a specific row, so naming the row is what keeps `status` comparing the right two things.
func (*Client) ListFeatureTables ¶ added in v0.27.0
ListFeatureTables reads every table belonging to one feature.
Shadow rows are excluded by the endpoint's own default, so this is live rows only — which is what a clone wants. Filtering to `derived` is left to the CALLER rather than done here: the skipped kinds have to be REPORTED ("not cloned (kinds: dynamic, integration)"), and a client that dropped them silently could not say what it skipped.
⚠️ Pagination is offset-based, so a table created or deleted mid-walk can shift a row across a page boundary and be seen twice or not at all. Nothing here compensates: the fix would be a stable cursor the API does not offer, and pretending otherwise (deduplicating, retrying) would hide the skew rather than remove it. A clone that missed a table reports one fewer file; it does not corrupt anything.
func (*Client) ListWorkflowFiles ¶
ListWorkflowFiles reads every file of ONE workflow row — note that a draft has its own complete file set, so the id passed here decides whether you get the live code or the draft's.
The route returns a bare JSON array ([]*rdb.WorkflowFile), not an envelope.
func (*Client) ListWorkflowRuns ¶ added in v0.27.0
func (c *Client) ListWorkflowRuns(ctx context.Context, workflowID string, limit int) ([]WorkflowRun, error)
ListWorkflowRuns reads a workflow's most recent runs, newest first.
The ordering is REQUESTED, not assumed: rworkflow.QueryRuns applies no default ORDER BY, so a limited query without one returns whatever rows Postgres happened to reach first — which for a "find the last failed run" caller is not a sort problem but a wrong answer. `executed_at desc` is validated server-side against the table's columns.
Runs belong to a workflow ROW, and a draft is its own row, so this is asked of the draft `wf test` runs rather than of the live workflow. That matches the server's own resume rule: the run being resumed must belong to the same workflow the resume is posted to.
func (*Client) ListWorkflowVersions ¶ added in v0.26.9
ListWorkflowVersions reads a workflow's committed version history, newest first — the server orders it (committed_at DESC, id DESC), which is the SAME total order rworkflow.resolveHeadVersionID resolves the head by, so element 0 is the head rather than merely a recent version.
It lives on the write side, like GetWorkflowFile, because it exists for the write side: the only caller is the commit CAS override, which needs the id of the version it is about to overwrite. The route answers a bare JSON array of rows and stamps no `url` on them (it is a LIST route), so treat these as identity and timing only.
func (*Client) Me ¶
Me identifies the caller. It is the CLI's login verification: the endpoint returns the user, and their tenant and role when one is selected.
Note that /api/v2/authentication is admin-scoped, so a SCOPED token cannot call it — which is why `login` mints a full-access Personal Access Token. A PAT still never exceeds its user's live role.
func (*Client) PollDevice ¶
func (c *Client) PollDevice(ctx context.Context, start *DeviceStart) (*DeviceToken, error)
PollDevice waits for a human to approve the flow, then returns the minted token. It blocks until approval, denial, expiry, or ctx cancellation.
The server's advertised interval is the starting cadence; a slow_down response backs it off, because the server enforces the throttle and an over-eager client would otherwise never make progress.
TRANSIENT failures are tolerated rather than fatal, and the tolerance is measured in ELAPSED TIME (MaxPollFailureWindow), not in retries. A login has a ~10 minute budget during which a human is walking to a browser, and in that window a rolling deploy, a 429 from the global rate limiter or a dropped connection are all entirely ordinary. Aborting on the first one would burn a flow the user then has to restart for no reason — and a retry COUNT quietly made the real tolerance a function of the cadence, so it came out shorter than an ordinary deploy takes. The RFC-defined outcomes (access_denied, expired_token, invalid_grant) are still terminal — those are the server's considered answer, not a blip. The deadline remains the backstop so this always terminates.
func (*Client) PreviewDataApp ¶ added in v0.27.0
func (c *Client) PreviewDataApp(ctx context.Context, appID string, in PreviewRequest) (*PreviewOutcome, error)
PreviewDataApp renders a data app headlessly and returns what was seen.
A busy harness comes back as *BusyError, NOT as a *PreviewOutcome with a flag: the two are different events (an observation versus the absence of one), and a single return value would let a caller read a busy answer as an app with no errors. Everything else non-2xx is an ordinary *Error.
func (*Client) PublishWorkflowDraft ¶
PublishWorkflowDraft takes a PARENTLESS draft live. Takes the draft's own id, which for a parentless draft is also the workflow's stable identity.
func (*Client) PutDataAppFile ¶
func (c *Client) PutDataAppFile(ctx context.Context, id, path, content string) (*DataAppFileSaveResponse, error)
PutDataAppFile creates or replaces one file, returning the saved row plus the recompile's diagnostics when it failed. See DataAppFileSaveResponse.
SLOW: every write recompiles the whole bundle with esbuild and uploads a fresh bundle to S3, so this is seconds of real work rather than a row write.
func (*Client) PutWorkflowFile ¶
func (c *Client) PutWorkflowFile(ctx context.Context, id, path, content string, baseSha256 *string) (*FileSaveResponse, error)
PutWorkflowFile creates or replaces one file.
This is the hard-validating path: unreachable `{{ ref }}` / `{{ write }}` / `{{ agent }}` / `{{ codex }}` markers roll the write back with a 400, and the file keeps its previous content. Secrets are the soft tier and come back as Warnings instead.
baseSha256 is the optional compare-and-swap precondition described on putWorkflowFileInput. A failed precondition is a 409, and the write is rolled back — the file certainly keeps its previous content, exactly like a 400.
The other SLOW call: every save re-derives the workflow's binding columns in the same transaction, so a file write is a row write with marker resolution attached — not something to hold to a read's deadline.
func (*Client) Query ¶
func (c *Client) Query(ctx context.Context, in QueryInput, timeout time.Duration) (*QueryResult, error)
Query runs one SQL statement and returns the csv_meta envelope.
timeout bounds the request. Zero or less means no client-side deadline at all, which the command surfaces as `--timeout 0` — a query that has been routed to Batch compute can legitimately outlast any number anyone would think to type, and being told to go and look for a result that is still coming is not an improvement on waiting for it.
func (*Client) RequestDataAppReview ¶
func (c *Client) RequestDataAppReview(ctx context.Context, draftID string) (*DataAppCommitOutcome, error)
RequestDataAppReview submits the caller's own edit draft for admin review.
Unlike the workflow equivalent this lives on the data-app handler itself rather than the governance one, so the path has no /draft/ segment.
It answers the same DataAppCommitOutcome shape as the commit route (with `proposed: false`), so the audience is available on BOTH endings of "take my draft live".
An older server answers this route with an EMPTY body, which is io.EOF to the decoder. That is not a failure — the review WAS submitted; only the narration is missing — so it degrades to a zero-value outcome rather than reporting a successful submission as an error.
io.ErrUnexpectedEOF is deliberately NOT tolerated, and the distinction is the whole point: io.EOF means the server sent NO body (nothing to narrate), while ErrUnexpectedEOF means it sent a body that was CUT OFF mid-way — a truncated response we have no business reading as "fine, just quiet". Widening the check to any EOF-ish error would swallow exactly the connection failures a publish path must surface.
func (*Client) RequestTableReview ¶ added in v0.27.0
RequestTableReview submits the caller's own draft for admin review. Takes the DRAFT's id, and is idempotent.
NOTE the different prefix, exactly as with RequestWorkflowReview: this route lives on the GOVERNANCE handler at /api/v2/table/draft/:id/request-review, whose group is scoped `admin` rather than the table loop's `data`. The CLI's full-access PAT covers both; a scoped token would half-work, which is why the failure is reported as-is rather than swallowed.
This is the only exit from the loop for a non-admin in a SHARED feature, where commit is refused.
func (*Client) RequestWorkflowReview ¶
RequestWorkflowReview submits the caller's own edit draft for admin review.
NOTE the different prefix: this route lives on the GOVERNANCE handler, whose group is scoped `admin` rather than the core workflow handler's `automation`. The CLI's full-access PAT covers both; a scoped token would half-work, which is why the failure is reported as-is rather than swallowed.
func (*Client) ResumeWorkflowRun ¶ added in v0.27.0
func (c *Client) ResumeWorkflowRun(ctx context.Context, workflowID, resumeOfRunID string) (*WorkflowRun, error)
ResumeWorkflowRun starts a run that RESUMES a failed one, replaying the lineage's journaled step results instead of re-executing the work they record. Like RunWorkflow it returns immediately at status "running".
It sends resumeOfRunID and NOTHING ELSE, which is the contract rather than a simplification. A resume inherits the target run's parameter values verbatim — the server copies them across — and refuses any parameterValues the caller supplies, because changed parameters would silently invalidate every step result computed under the old ones. So this deliberately does not share RunWorkflow's body struct: there, an empty map is sent to avoid a null; here, a parameterValues key is a thing to be unable to send at all.
The server owns the rest of the rules (the target must be a failed run of THIS workflow, its lineage must have no run already running and none that has succeeded, and a gated workflow cannot be resumed) and its refusals are passed through verbatim.
func (*Client) RunWorkflow ¶
func (c *Client) RunWorkflow(ctx context.Context, workflowID string, parameterValues map[string]any) (*WorkflowRun, error)
RunWorkflow starts a run and returns IMMEDIATELY with the created run row at status "running" — execution is asynchronous, and GetWorkflowRun is how you learn what happened.
Two refusals arrive here as 400s with the server's own message: an approval gate (which no HTTP caller can satisfy — only an agent session produces an approval) and the per-tenant credit kill-stop. Both are passed through rather than paraphrased.
parameterValues is always sent, as {} when there are none. NOT `omitempty`: that omits the key, which is the null map it is meant to avoid — the server decodes a missing key to a nil map exactly as it does an explicit null.
func (*Client) StartDevice ¶
StartDevice opens a device-authorization flow. No authentication required.
func (*Client) SyncTable ¶ added in v0.27.0
SyncTable triggers a DETERMINISTIC materialization: the server executes the stored `code` exactly as written, with no AI and no code rewriting.
This is the golden path, and the distinction from POST /:id/build matters enough to state: `/build` is AI-ASSISTED and may REWRITE the stored SQL, which for a folder-synced table would silently replace the file's own content. The CLI never calls it.
Two further properties, both of which shape the loop around this call:
- It syncs EXACTLY the row named. It does not mint a draft (`/build` does), so the id passed here decides whether the live table or the draft is materialized.
- The 200 means ACCEPTED, not started successfully. Every failure — down to syncing a table with no code at all — is reported on the ROW, never in this response. WaitForTableBuild is the other half of this call.
A sync does NOT cascade: dependent tables are not rebuilt. Commit is what cascades.
func (*Client) UpdateDataApp ¶
func (c *Client) UpdateDataApp(ctx context.Context, id string, patch DataAppPatch) (*DataApp, error)
UpdateDataApp patches a data app's metadata and allowlists.
Returns the row that RECEIVED the edit, which is not necessarily the one addressed: patching a live app auto-forks a draft, exactly as a file write does. Callers that care which row they changed must read the returned id.
func (*Client) UpdateTableCode ¶ added in v0.27.0
UpdateTableCode writes a table's SQL and its declared inputs.
`id` is the row being written, which in the edit loop is always the DRAFT's id. Writing the LIVE row instead is silently lost work: committing a draft overwrites the parent's fields from the draft, so a PUT on the canonical vanishes the moment anyone commits.
The code sent should be the CANONICAL id-ref form (see tablerefs). A positional ref written here is re-resolved server-side against whatever inputModels this same call declares, which is not necessarily the list it was numbered against.
The endpoint answers with nothing, so a caller that needs the row's current state re-reads it.
func (*Client) UpdateWorkflow ¶
UpdateWorkflow patches a workflow's metadata.
The endpoint answers with nothing, and it DROPS the save-time warnings the file path returns (the server's own comment says to plumb a new endpoint rather than overload this one) — so a caller that needs the row's current state re-reads it afterwards.
func (*Client) ValidateDataApp ¶
ValidateDataApp recompiles a draft and stamps it validated, which CommitDataAppDraft refuses without. Takes the DRAFT id.
This is the authoritative answer to "does this compile": it always recompiles from the row's current files, so a draft left un-validated by a failed file write is re-checked here rather than trusted.
SLOW for the same reason PutDataAppFile is.
func (*Client) ValidateWorkflowFiles ¶
func (c *Client) ValidateWorkflowFiles(ctx context.Context, in ValidateInput) (*ValidateResult, error)
ValidateWorkflowFiles dry-runs a candidate workflow. Findings are data: a candidate full of errors still comes back as a successful call, and only a malformed request (no featureID, a feature the caller cannot read) is an error here.
One of the two SLOW calls: the whole folder goes up in one body and every marker in it is resolved against the caller's access, so this is doing real work rather than reading a row.
func (*Client) WaitForTableBuild ¶ added in v0.27.0
WaitForTableBuild polls a table until its build reaches a verdict.
EVERY command polls through this one function, so the `ready`-with-an-error trap is decoded in exactly one place. Pass the id of the row that was actually synced — in the pipeline loop that is always the DRAFT, which is also what keeps the draft-aware LastBuildError describing the row being waited on.
A `failed` or `failed_stale` verdict comes back as a RESULT, not an error. Errors are reserved for losing contact with the instance, a cancelled context, and the deadline.
The deadline case names the stuck-`pending` cause specifically, because it is the one failure with no error row to read: syncing a table with no `code` parks it at `pending` forever, and the server reports that nowhere else.
type CommitDraftInput ¶ added in v0.26.9
type CommitDraftInput struct {
ConfirmHeadVersionID string `json:"confirmHeadVersionID,omitempty"`
}
CommitDraftInput is the OPTIONAL body of POST /workflow/:id/commit, mirroring rworkflow.CommitDraftInput (backend/resource/rworkflow/model.go).
An empty body is the normal case and means "no override": the commit then requires the parent to still be at the version this draft was seeded from, and is refused with a 409 when it is not.
ConfirmHeadVersionID acknowledges the specific committed version this commit will OVERWRITE. It must equal the head AT COMMIT TIME, not merely be non-empty — a caller who learns the head, deliberates, and commits while a third version lands gets another 409. That re-refusal is the feature, which is why nothing here retries.
type CompileDiagnostic ¶
type CompileDiagnostic struct {
Message string `json:"message"`
Line int `json:"line"`
Column int `json:"column"`
File string `json:"file,omitempty"`
}
CompileDiagnostic is one author-readable build error, mirroring dataappbundle.Diagnostic.
type CompileError ¶
type CompileError struct {
Message string `json:"message"`
Diagnostics []CompileDiagnostic `json:"diagnostics"`
}
CompileError is a failed bundle compile, mirroring the api/v2/dataapp CompileDiagnostics response type.
type CreateDataAppInput ¶
type CreateDataAppInput struct {
FeatureID string `json:"featureID"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
DataAppAccess
}
CreateDataAppInput is the body of POST /dataapp, mirroring the subset of rdataapp.CreateInput the CLI writes.
What this call PRODUCES matches the workflow equivalent as of migration 000495: a hidden PARENTLESS DRAFT, visible only to its drafter, which the first commit promotes to live IN PLACE — so the id this returns is the app's id forever and is safe to record in ronja.json before any publish. (Before 000495 the row was born LIVE and empty, serving the pending page to the whole feature until someone published it.)
The allowlists are sent at creation because a data app with none can query nothing, and the alternative (create, then patch) leaves a window where the app exists in a state its author never asked for.
type CreateTableInput ¶ added in v0.27.0
type CreateTableInput struct {
Name string `json:"name"`
FeatureID string `json:"featureID"`
// Kind is always TableKindDerived from this loop. A pipeline folder is a
// folder of SQL; a kind that is not built from SQL has nothing a .sql file
// could mean.
Kind string `json:"kind"`
Engine string `json:"engine"`
Code string `json:"code"`
InputModels []string `json:"inputModels"`
}
CreateTableInput is the body of POST /feature/model, mirroring the subset of feature.CreateTableInput the CLI writes.
Six fields, where the server's request type has fifteen. Everything else is either build state, draft lifecycle, metric recipe or verification — none of it the CLI's to set, and the server's own type comment explains why the fat body was narrowed in the first place.
InputModels is sent even though the server would derive it from the refs in `code`. That is the same rule as on the update path and for the same reason — see UpdateTableCode — plus one specific to create: the derivation and the access check run off different inputs, and sending the list makes the lineage this folder DECLARES the lineage the row gets, rather than whatever a regex found.
type CreateWorkflowInput ¶
type CreateWorkflowInput struct {
FeatureID string `json:"featureID"`
Title string `json:"title"`
Entrypoint string `json:"entrypoint"`
// Parameters is the declared parameter set, when the folder manages one.
// omitempty because a folder that does not manage parameters must create a
// workflow with none rather than assert an empty declaration.
Parameters []WorkflowParameter `json:"parameters,omitempty"`
// RuntimeVersion is 1 or 2. Stamped at CREATE here, and afterwards raisable
// one way (1 -> 2) through WorkflowPatch — never lowered.
//
// omitempty, so a v1 folder's create body is byte-identical to the one the
// CLI sent before durable workflows existed — an absent key is 1 server-side.
RuntimeVersion int `json:"runtimeVersion,omitempty"`
// ReportingTimezone is the declared execution calendar, when the folder
// manages one. A plain string with omitempty rather than the patch's pointer,
// because the three-state distinction does not exist here: the CREATE
// endpoint REFUSES an explicit empty string (reportingtz.ParseDeclared
// rejects it) and stamps the organization's default when the key is absent,
// which is exactly what an unmanaged folder wants. A folder declaring the
// reset value sends the literal "UTC" instead — see effectiveDeclaredZone.
ReportingTimezone string `json:"reportingTimezone,omitempty"`
}
CreateWorkflowInput is the body of POST /workflow.
The server's input type is the whole rdb.Workflow, and this deliberately is not: everything else on that row is either derived (the binding columns are stamped from the code at file save), stamped by the server (drafter attribution is scrubbed from HTTP input on purpose), or not the CLI's to set. Sending three fields makes it obvious that a first push creates an EMPTY workflow whose bindings then come from its files.
type DataApp ¶
type DataApp struct {
ID string `json:"id"`
FeatureID string `json:"featureID"`
Name string `json:"name"`
Description string `json:"description"`
// Entrypoint names the file the bundle compiles from. Reported, never
// pushed: rdataapp.Patch has no entrypoint field, so a data app's is fixed
// at "App.tsx" from the moment the row is created.
Entrypoint string `json:"entrypoint"`
// BundleFileKey is the compiled bundle in S3, empty on an app that has never
// compiled. Read as a FACT ABOUT THE PAST only — a failed compile leaves the
// previous key in place, so a non-empty value never means "the current source
// compiles". ValidatedAt is the field that answers that.
BundleFileKey string `json:"bundleFileKey"`
// ValidatedAt is cleared by every file mutation and re-stamped only by a
// clean compile. It is the single gate POST :id/commit enforces, so it is
// also what `ronja app publish` checks before it tries.
ValidatedAt *time.Time `json:"validatedAt"`
DataAppAccess
// ParentDataAppID is empty on a live row AND on a parentless draft (an app
// created by POST /dataapp and not yet published, since migration 000495),
// and set on an edit shadow or a committed version snapshot. So it — not
// the lifecycle, which reads "draft" for both — is what tells a never-
// published app apart from somebody's open edits to a live one.
ParentDataAppID string `json:"parentDataAppID"`
CommittedAt *time.Time `json:"committedAt"`
// Lifecycle is trigger-computed server-side: live|draft|version|archived|proposed.
Lifecycle string `json:"lifecycle"`
// SubmittedForReviewAt is set once a drafter asks an admin to commit their
// draft; null on every other row, and on a draft they have not submitted.
SubmittedForReviewAt *time.Time `json:"submittedForReviewAt"`
DrafterUserID string `json:"drafterUserID"`
CreatedBy string `json:"createdBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// URL is the absolute frontend /apps/<id> page, stamped by the SERVER
// (api/v2/dataapp.DataAppView) on the single-row routes. Never built here —
// see the same field on Workflow for why, and for why an empty value is
// normal, silent, and must not be filled in with a guess.
URL string `json:"url,omitempty"`
}
DataApp is a HAND-MIRROR of the subset of rdb.DataApp the CLI reads.
Same rule and same hazard as Workflow in workflow.go: a json tag that does not exist on the wire decodes to the zero value SILENTLY, so a typo here shows up as an empty column of output — or as an allowlist the CLI believes is empty and then pushes as empty, revoking the app's access to its own tables. Every tag below is copied from backend/ronja/rdb/table_data_app.go; keep them in step with that file, and see dataapp_test.go for the fixture that checks them.
The lifecycle vocabulary is shared with workflows (rdb.DataAppLifecycle carries the same five values as rdb.WorkflowLifecycle), so the Lifecycle* constants and DescribeLifecycle are reused rather than duplicated.
Mirroring decisions specific to this row:
- The server's optional.V[T] marshals as the bare value or JSON `null`, and unmarshalling `null` into a non-pointer is a documented no-op — so optional STRINGS are plain strings ("" is exactly what "unset" means for an id) while optional TIMESTAMPS are pointers, because "has this been validated at all" is the question the whole publish path turns on.
- The allowlist slices carry NO omitempty server-side (they are plain []string columns), so they arrive as [] rather than absent. Normalize still runs, because a nil from a hand-written fixture or an older instance would otherwise range differently from an empty one.
func (*DataApp) IdentityID ¶
IdentityID is the STABLE id of the data app this row belongs to: the parent for a draft or a version snapshot, the row itself for a live app.
This is what a manifest binding records. A draft's own id is per-user and disappears on commit, so a committed file naming one would point at a row that stops existing the moment its author publishes.
func (*DataApp) IsValidated ¶
IsValidated reports whether the row's current source has compiled cleanly since its last file edit — the gate POST :id/commit enforces.
type DataAppAccess ¶
type DataAppAccess struct {
AllowedTableIDs []string `json:"allowedTableIDs"`
AllowedSecretIDs []string `json:"allowedSecretIDs"`
AllowedAgentIDs []string `json:"allowedAgentIDs"`
AllowedWorkflowIDs []string `json:"allowedWorkflowIDs"`
AllowedCodexIDs []string `json:"allowedCodexIDs"`
AllowedMetricIDs []string `json:"allowedMetricIDs"`
// Capabilities are the coarse switches (query_ronja, run_agent, …) the
// minted script-token carries alongside the id allowlists.
Capabilities []string `json:"capabilities"`
}
DataAppAccess is a data app's capability surface: what its bundle may reach.
One type serving three roles on purpose — it is embedded in the row, it is the manifest's `access` block, and it is what the patch sends. A second struct would only add a conversion that silently drops whatever field was added to one side and not the other, and this is the type where a dropped field means a REVOKED capability rather than a cosmetic gap.
Its json tags are therefore part of the ronja.json FILE FORMAT as well as the wire, and renaming one is a breaking change to committed manifests.
These are grants, not descriptions: an id here is a capability the app's viewers inherit, which is why the server re-checks every one of them against the pusher's own access (governance.ValidateDataAppScope) rather than trusting the file.
func (DataAppAccess) IsEmpty ¶
func (a DataAppAccess) IsEmpty() bool
IsEmpty reports an access set that grants nothing.
func (DataAppAccess) Normalized ¶
func (a DataAppAccess) Normalized() DataAppAccess
Normalized returns a copy with nil lists replaced by empty ones and every list sorted and de-duplicated.
Sorting is what makes SameAccess a comparison of MEANING rather than of spelling. An allowlist is a set — the server stores it as one and the token mint reads it as one — so a manifest listing two tables in the other order is the same grant, and treating it as drift would make `app status` report a change forever and `app push` send a patch that changes nothing.
type DataAppAudience ¶ added in v0.28.2
type DataAppAudience struct {
// Reach is "author" (a private feature — an audience of one) or
// "organization" (everyone in the org can open it).
Reach string `json:"reach"`
// FeatureName is the feature that DECIDES the reach; an app has no scope of
// its own.
FeatureName string `json:"featureName"`
// Reads / Calls / Writes are the app's bindings, labelled. Writes is
// non-empty only when the app can write to a managed database — every
// viewer gets it, since an app cannot tell viewers apart.
Reads []string `json:"reads"`
Calls []string `json:"calls"`
Writes []string `json:"writes"`
// Sentence is the line to print.
Sentence string `json:"sentence"`
}
DataAppAudience mirrors rdataapp.AudienceSummary.
The SENTENCE is composed server-side and printed verbatim. Composing one here would be a second statement of "who can do what", and two statements of one grant drift — the CLI would eventually describe an app's powers differently from the app's own publish response and from what Ronja tells the user in chat.
type DataAppCommitOutcome ¶
type DataAppCommitOutcome struct {
DataAppID string `json:"dataAppID"`
FirstPublish bool `json:"firstPublish"`
Proposed bool `json:"proposed"`
// Audience is who ends up able to open the app and what they can do — the
// question publish never answered, and the reason two customers shipped a
// system of record to an audience of one, or handed a whole organization a
// managed database's write role without being told.
Audience *DataAppAudience `json:"audience,omitempty"`
}
DataAppCommitOutcome is the body of POST /dataapp/:id/commit, mirroring rdataapp.CommitOutcome.
It exists because the route has TWO successful endings and they are not the same event: a commit either publishes, or — for a non-admin publishing a brand-new app into a shared feature — raises a proposal an admin must approve. `proposed` is the only thing on the wire that tells them apart, so a caller that ignores this body necessarily reports one of them wrongly.
type DataAppFile ¶
type DataAppFile struct {
ID string `json:"id"`
DataAppID string `json:"dataAppID"`
Path string `json:"path"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
DataAppFile mirrors rdb.DataAppFile — one source file of one data-app row. There is no content hash on the wire, so the CLI hashes Content itself.
type DataAppFileDeleteResponse ¶
type DataAppFileDeleteResponse struct {
DataAppID string `json:"dataAppID"`
CompileError *CompileError `json:"compileError,omitempty"`
}
DataAppFileDeleteResponse is the body of DELETE :id/files/*path. DataAppID is the row the delete landed on — deleting from a live app auto-forks a draft.
type DataAppFileSaveResponse ¶
type DataAppFileSaveResponse struct {
DataAppFile
CompileError *CompileError `json:"compileError,omitempty"`
}
DataAppFileSaveResponse is the body of PUT :id/files/*path.
The compile failure arrives IN a 200 rather than as an error, and that is the server being accurate rather than lenient: the file write COMMITS before the bundle is recompiled, so the save really did happen. Pushing a multi-file app walks through states that do not compile by construction — App.tsx importing a module that is one request away — so a client that treated this as a failure could not push more than one file.
What it does NOT mean is that the app is fine. validated_at was cleared by the write and only a clean compile re-stamps it, so ValidateDataApp is the verdict and CommitDataAppDraft refuses until it passes.
type DataAppPatch ¶
type DataAppPatch struct {
Name *string `json:"name,omitempty"`
AllowedTableIDs *[]string `json:"allowedTableIDs,omitempty"`
AllowedSecretIDs *[]string `json:"allowedSecretIDs,omitempty"`
AllowedAgentIDs *[]string `json:"allowedAgentIDs,omitempty"`
AllowedWorkflowIDs *[]string `json:"allowedWorkflowIDs,omitempty"`
AllowedCodexIDs *[]string `json:"allowedCodexIDs,omitempty"`
AllowedMetricIDs *[]string `json:"allowedMetricIDs,omitempty"`
Capabilities *[]string `json:"capabilities,omitempty"`
}
DataAppPatch is the body of PUT /dataapp/:id, mirroring the subset of rdataapp.Patch the CLI writes.
Every field is a POINTER because every field of the server's Patch is an optional.V: an absent key means "unchanged", while an explicit value — including an empty list — REPLACES what is there. Both are things a folder legitimately means ("I do not manage the allowlists" and "this app is allowed nothing"), and a plain slice with omitempty collapses them into the first, making it impossible to remove the last table from an app.
There is deliberately no Entrypoint: rdataapp.Patch has none, because a data app's entrypoint is stamped at create and never moves.
func (DataAppPatch) Empty ¶
func (p DataAppPatch) Empty() bool
Empty reports a patch that would change nothing, so the caller can skip the round trip rather than send `{}`.
func (*DataAppPatch) SetAccess ¶
func (p *DataAppPatch) SetAccess(access DataAppAccess)
SetAccess fills in every allowlist field from a declared set.
All seven or none: the allowlists are one decision, and patching a subset would let a folder that manages access leave a list it happens not to use pointing at whatever the row had.
type DatabaseSQLInput ¶
type DatabaseSQLInput struct {
SQL string `json:"sql"`
// Params are positional bound parameters for $1, $2 … placeholders, carried
// as RAW JSON rather than []any.
//
// That is load-bearing, not fussiness. Decoding a parameter list into []any
// turns every number into a float64, which silently loses precision above
// 2^53 — 9007199254740993 becomes ...992, and 1234567890123456789 becomes
// ...800. Re-encoding cannot recover what the decode threw away, so the wrong
// number would reach the database with no error at any layer. Passing the
// user's bytes through untouched means a bigint id survives the trip.
//
// Sent only when non-empty: the field is optional server-side, and an
// explicit null adds nothing.
Params []json.RawMessage `json:"params,omitempty"`
MaxRows int `json:"maxRows,omitempty"`
}
DatabaseSQLInput is the POST :id/sql body.
type DatabaseSQLResult ¶
type DatabaseSQLResult struct {
DatabaseID string `json:"databaseID"`
// Result is the CSV, header row included. Empty for a statement with no
// result set — a plain INSERT, say — which is not the same as no rows.
Result string `json:"result"`
RowCount int `json:"rowCount"`
// Truncated reports that the row cap cut the result off, so Result is a
// prefix rather than the answer.
Truncated bool `json:"truncated"`
DurationMs int64 `json:"durationMs"`
}
DatabaseSQLResult is the POST :id/sql response.
No omitempty anywhere: this struct is emitted verbatim by `ronja db sql --json`, and a field that vanishes when it is zero makes the payload a different shape depending on the answer.
type DeviceStart ¶
type DeviceToken ¶
type DeviceToken struct {
Token string `json:"token"`
TokenID string `json:"tokenID"`
TokenName string `json:"tokenName"`
// UserID and TenantID are unused by the CLI today. They are kept because
// this struct is a faithful mirror of the server's response, and a mirror
// that quietly omits fields is one nobody can check against the source.
UserID string `json:"userID"`
TenantID string `json:"tenantID"`
// ExpiresAt is RFC 3339. CLI-minted tokens are time-bounded, and a
// credential that stops working with no warning is a bad surprise.
ExpiresAt string `json:"expiresAt"`
}
type Error ¶
Error is a non-2xx response. Code carries the machine-readable `error` field when the server sent one — the CLI branches on it for the device-flow poll, where "authorization_pending" is an expected, non-fatal answer.
type Feature ¶
type Feature struct {
ID string `json:"id"`
Name string `json:"name"`
// Scope is private|workspace|organization. Compare against
// scopeOrganization; an unknown value is treated as not-shared by callers,
// which fails toward attempting the commit and letting the server decide.
Scope string `json:"scope"`
}
Feature is a HAND-MIRROR of the sliver of rdb.Feature the CLI reads: what a feature is called, and how private it is.
Scope is the only field anything branches on, and it decides whether a commit is admin-only. Workflows get it for free — the server stamps featureScope onto every workflow row at read time — but rdb.DataApp carries no such column, so `ronja app publish` has to ask. That asymmetry is the entire reason this type exists; it is not a general feature client and should not grow into one, since discovery belongs on plain HTTP.
Tags copied from backend/ronja/rdb/table_feature.go.
type File ¶ added in v0.27.0
type File struct {
ID string `json:"id"`
Name string `json:"name"`
FileKey string `json:"fileKey"`
ContentType string `json:"contentType"`
ContentSize int64 `json:"contentSize"`
}
File is a HAND-MIRROR of the sliver of rdb.File a preview frame needs.
Narrow on purpose, and for the same reason Feature is: this is not a general file client, and discovery belongs on plain HTTP. It exists because a preview hands back FILE IDS and the bytes live behind a second call keyed on FileKey — which is the only field here anything branches on.
Tags copied from backend/ronja/rdb/table_file.go.
type FileDeleteResponse ¶
type FileDeleteResponse struct {
Warnings []string `json:"warnings,omitempty"`
}
FileDeleteResponse mirrors the handler's FileDeleteResponse. Deleting the last file that referenced a secret drops that binding, which is exactly the kind of change nobody asked for that a warning exists to surface.
type FileSaveResponse ¶
type FileSaveResponse struct {
WorkflowFile
Warnings []string `json:"warnings,omitempty"`
}
FileSaveResponse mirrors the workflow handler's FileSaveResponse: the saved file at the response root, plus soft save-time notices.
Warnings is the SOFT tier of the save path — an unreachable secret is filtered out of the binding rather than refused — and it is the only place those notices exist, so the CLI passes them through verbatim rather than paraphrasing.
type Me ¶
type Me struct {
User struct {
ID string `json:"id"`
Email string `json:"email"`
// Serialized from optional.V[string], so these are `null` rather than
// absent when unset; encoding/json leaves the zero value in place.
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
} `json:"user"`
Tenant *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"tenant,omitempty"`
Role *struct {
Name string `json:"name"`
PrivilegeLevel int `json:"privilegeLevel"`
} `json:"role,omitempty"`
IsRonjaStaff bool `json:"isRonjaStaff"`
}
Me mirrors the /authentication/me response (MeResponse in backend/api/v2/authentication/api_login.go). Tenant and Role are absent for a user who has not selected an organization.
This is a HAND-MIRROR of server types, so the field names have to come from the structs that are actually serialized, not from what a CLI would like to receive:
- User is a *rdb.User — backend/ronja/rdb/table_user.go is the source of truth. It has first_name / last_name and NO "name" field.
- Role is a *rdb.Role — backend/ronja/rdb/table_roles.go — which is keyed on name and has NO id column.
Keeping this in step with those files is load-bearing (see CLAUDE.md): a field that does not exist on the wire decodes to the zero value silently, so the mistake shows up as a blank line of output rather than an error.
type Migration ¶
type Migration struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
// SQL is sent VERBATIM. The server's ledger hash covers exactly these bytes,
// so trimming or normalising here — a trailing newline is the obvious
// temptation — would make a file hash differently from the way it was
// applied, and every applied migration would then report as drifted.
SQL string `json:"sql"`
}
Migration is one named migration in a set.
type MigrationResult ¶
type MigrationResult struct {
Name string `json:"name"`
Outcome string `json:"outcome"`
Seq int `json:"seq"`
}
MigrationResult is one migration's verdict.
type MigrationSetInput ¶
type MigrationSetInput struct {
Migrations []Migration `json:"migrations"`
DryRun bool `json:"dryRun,omitempty"`
}
MigrationSetInput is the POST :id/migrations body.
type MigrationSetResult ¶
type MigrationSetResult struct {
DatabaseID string `json:"databaseID"`
DryRun bool `json:"dryRun"`
Results []MigrationResult `json:"results"`
}
MigrationSetResult is the POST :id/migrations response.
func (*MigrationSetResult) Drifted ¶
func (r *MigrationSetResult) Drifted() []MigrationResult
Drifted returns the migrations the server reported as drifted.
Only a DRY RUN can produce these: a real apply refuses the whole set and returns an error instead, which is why `db migrate push` never needs to inspect this and `db migrate status` always does.
func (*MigrationSetResult) Pending ¶
func (r *MigrationSetResult) Pending() []MigrationResult
Pending returns the migrations a real apply would apply.
type PreviewBusy ¶ added in v0.27.0
type PreviewBusy struct {
Busy bool `json:"busy"`
Message string `json:"message"`
Calibration string `json:"calibration"`
}
PreviewBusy is the 429 body: the shared render pool (or this caller's own per-minute budget) was spent, so NOTHING about the app was observed.
type PreviewConsoleError ¶ added in v0.27.0
type PreviewConsoleError struct {
Message string `json:"message"`
}
PreviewConsoleError is a deduped console.error call. Message carries the same <untrusted_app_output> envelope PreviewRuntimeError.Message does, and for a blunter reason: a console.error commonly prints a whole tenant row.
type PreviewHarnessRequest ¶ added in v0.27.0
type PreviewHarnessRequest struct {
URL string `json:"url"`
Method string `json:"method"`
Status int `json:"status"`
Ms int64 `json:"ms"`
}
PreviewHarnessRequest is one service-API request as the BROWSER saw it — the fallback view for a bundle with no op ledger.
type PreviewNetworkFailure ¶ added in v0.27.0
type PreviewNetworkFailure struct {
URL string `json:"url"`
Status int `json:"status"`
StatusText string `json:"statusText"`
}
PreviewNetworkFailure is a 4xx/5xx or transport failure, after platform-CDN noise has been filtered out server-side.
type PreviewOp ¶ added in v0.27.0
type PreviewOp struct {
Path string `json:"path"`
StartedAt int64 `json:"startedAt"`
Ms int64 `json:"ms"`
Rows *int64 `json:"rows,omitempty"`
OK bool `json:"ok"`
}
PreviewOp is one entry of the SDK op ledger — a query, agent run or file read the app performed, timed from inside the page.
Rows is a POINTER because nil ("this operation has no row count to report") and 0 ("it returned nothing") are different facts, and a preview exists partly to tell a blank page apart from an empty result.
type PreviewOutcome ¶ added in v0.27.0
type PreviewOutcome struct {
Result *PreviewResult
Raw []byte
}
PreviewOutcome pairs one decoded observation with the bytes it arrived as.
The raw bytes are not a convenience. This file is a hand-mirror taken at one moment, and an instance newer than the CLI sends fields it does not know about; a report written by re-marshalling PreviewResult would silently drop exactly those, which is the drift the mirror convention exists to bound. So `ronja app test` writes report.json from Raw and reads Result only to summarise — the file is always at least as complete as the server's answer.
type PreviewRequest ¶ added in v0.27.0
type PreviewRequest struct {
// Route is an in-app hash fragment ("#/orders"). The server adds a missing
// '#' and refuses a control character.
Route string `json:"route,omitempty"`
// ViewportPreset is "desktop" (the default) or "mobile". The server resolves
// anything else to desktop rather than failing, which is exactly why the CLI
// refuses an unknown value locally: a typo that silently renders the wrong
// size answers a question nobody asked.
ViewportPreset string `json:"viewportPreset,omitempty"`
// TimeoutMs is the render budget, clamped server-side to the deployed
// harness's ceiling and never refused. Zero means "the server's default".
TimeoutMs int `json:"timeoutMs,omitempty"`
// Steps is an optional interaction plan run after the app renders.
//
// ⚠️ USR_ADMIN only, and it EXECUTES FOR REAL with viewer authority: a click
// runs the app's own code, so it can dispatch workflows and agents, upload
// files and write data. Nothing is mocked and nothing is rolled back.
Steps []PreviewStep `json:"steps,omitempty"`
}
PreviewRequest is the body of POST :id/preview. Every field is optional; an empty body renders the app's default route at desktop size.
type PreviewResult ¶ added in v0.27.0
type PreviewResult struct {
Rendered bool `json:"rendered"`
DurationMs int64 `json:"durationMs"`
// Viewport and Route are what the harness CONFIRMED it applied — never a
// restatement of what this CLI asked for. Both are ABSENT when the harness
// did not echo the settings it used (which is every deployed harness that
// predates the echo) and when nothing was rendered at all, so an empty value
// means "we cannot tell", not "desktop" and not "the default route".
//
// ⚠️ Printing the REQUEST here instead would be the one genuinely misleading
// thing this report could say: a caller checking a mobile layout would read
// its own argument back and conclude the mobile render was observed.
Viewport string `json:"viewport,omitempty"`
Route string `json:"route,omitempty"`
// Settle says WHY the harness stopped waiting, and is the first thing worth
// reading: "deadline" means the frame shows a page that was still working.
Settle *PreviewSettle `json:"settle,omitempty"`
Ops []PreviewOp `json:"ops"`
Errors []PreviewRuntimeError `json:"errors"`
ConsoleErrors []PreviewConsoleError `json:"consoleErrors"`
NetworkFailures []PreviewNetworkFailure `json:"networkFailures,omitempty"`
// AppOutputNote is the server's static one-liner about app-authored output:
// that every `message` on Errors and ConsoleErrors is text the APP produced
// and arrives wrapped in <untrusted_app_output>, to be read as evidence about
// the render rather than as an instruction.
//
// Present ONLY when there is such output to describe — an observation with no
// errors carries no note, because a note attached to nothing would read as a
// claim that some other string here is untrusted.
AppOutputNote string `json:"appOutputNote,omitempty"`
// HarnessRequests is the outside-in view of the app's service-API traffic,
// carried ONLY when the op ledger is absent (a bundle compiled before the
// ledger shipped). It cannot report row counts.
HarnessRequests []PreviewHarnessRequest `json:"harnessRequests,omitempty"`
// CompileDiagnostics is populated INSTEAD of a render when the bundle did not
// build. Still an observation: the call is a 200 and Errors is empty, which
// is why a caller gating on Errors alone would call a broken build clean.
CompileDiagnostics []CompileDiagnostic `json:"compileDiagnostics,omitempty"`
// Steps is one entry per interaction the VALIDATOR reported, in plan order.
// Absent when no plan was sent — and also when a validator predating steps
// support ignored one, which is what StepsNote exists to say out loud.
Steps []PreviewStepResult `json:"steps,omitempty"`
StepsNote string `json:"stepsNote,omitempty"`
ScreenshotFileIDs []string `json:"screenshotFileIDs,omitempty"`
// SelectedScreenshotFileID names the ONE representative frame — the frame the
// in-product agent is handed as an image. Selection is by PHASE preference,
// not by position, so "the last id" is not it.
SelectedScreenshotFileID string `json:"selectedScreenshotFileID,omitempty"`
StepScreenshotFileIDs []string `json:"stepScreenshotFileIDs,omitempty"`
Hint string `json:"hint,omitempty"`
Calibration string `json:"calibration"`
// DataAppID is the row that was ACTUALLY rendered: the caller's own open
// draft when they have one, which is not necessarily the id in the path.
DataAppID string `json:"dataAppID"`
// Screenshots is the persisted filmstrip in capture order. Empty when no
// frame was captured or the instance has no bucket wired.
Screenshots []PreviewScreenshot `json:"screenshots"`
}
PreviewResult is one render's observation — api/v2/dataapp.PreviewResult flattened, since the server embeds tools.PreviewObservation into it.
Ops / Errors / ConsoleErrors carry no omitempty server-side, so "no errors" arrives as an explicit `[]` rather than an absent key. Mirrored the same way so a re-marshal of this struct says the same thing the server did.
type PreviewRuntimeError ¶ added in v0.27.0
type PreviewRuntimeError struct {
// Message arrives WRAPPED in <untrusted_app_output> — see AppOutputNote. The
// wrap is the server's, and the raw bytes keep it: report.json is written
// from PreviewOutcome.Raw, so what a reader opens is exactly what the server
// framed. Only the terminal summary unwraps it, and only for display.
//
// Stack / RawStack / Source / Line / Column are NOT wrapped. They are Ronja's
// reading of the bundle's own source map rather than a string the app chose.
Message string `json:"message"`
Stack string `json:"stack,omitempty"`
RawStack string `json:"rawStack,omitempty"`
Source string `json:"source,omitempty"`
Line int `json:"line,omitempty"`
Column int `json:"column,omitempty"`
}
PreviewRuntimeError is one uncaught error or unhandled rejection. Source / Line / Column are the first source-map-resolved frame inside the author's own code, so a reader has "App.tsx:270:28" without parsing a stack.
type PreviewScreenshot ¶ added in v0.27.0
type PreviewScreenshot struct {
FileID string `json:"fileID"`
// FileKey is what the bytes are actually fetched by, and it is the field to
// prefer: /api/v2/file/download/:key is `data`-scoped like the preview
// itself, whereas resolving an id through /api/v2/feature/file/:id needs a
// route whose group carries no scope at all — which a SCOPED token is
// refused on. Empty only against an instance older than this field.
FileKey string `json:"fileKey"`
// Phase is the NEWEST phase these pixels were observed at: "first_paint" /
// "settled" / "deadline", or "step-<n>" for a `screenshot` step's frame.
Phase string `json:"phase"`
// Selected marks the ONE representative frame, on the wire so the CLI, the
// chat card and the model all cite the same picture.
Selected bool `json:"selected"`
}
PreviewScreenshot is one persisted filmstrip frame.
An id and a storage key, not a presigned URL: the caller fetches the bytes with the credential it already has. See DownloadFrame.
type PreviewSettle ¶ added in v0.27.0
type PreviewSettle struct {
// Signal is "sdk" (the app's own op ledger reported everything finished),
// "heuristic" (network and DOM went quiet) or "deadline" (the budget ran
// out, so the frame shows a page still working).
Signal string `json:"signal"`
PendingOpsAtCapture int `json:"pendingOpsAtCapture"`
DOMQuietMs int64 `json:"domQuietMs"`
InFlightAtCapture int `json:"inFlightAtCapture"`
PendingHosts []string `json:"pendingHosts,omitempty"`
OpsStarted int `json:"opsStarted"`
FirstRenderAt int64 `json:"firstRenderAt"`
LastSettledAt int64 `json:"lastSettledAt"`
ReportedReadyAt int64 `json:"reportedReadyAt"`
PageTimeOriginMs int64 `json:"pageTimeOriginMs"`
}
PreviewSettle mirrors jsvalidator.SettleInfo.
⚠️ TWO CLOCKS. DOMQuietMs is the HARNESS's clock; FirstRenderAt / LastSettledAt / ReportedReadyAt are the PAGE's (performance.now()). They are not comparable without PageTimeOriginMs. The CLI reads only Signal, and anything that starts subtracting one from the other must read that note in backend/platform/jsvalidator/types.go first.
type PreviewStep ¶ added in v0.27.0
type PreviewStep struct {
Action string `json:"action"`
Selector string `json:"selector,omitempty"`
Text string `json:"text,omitempty"`
Value string `json:"value,omitempty"`
TimeoutMs int `json:"timeoutMs,omitempty"`
}
PreviewStep is one requested interaction, mirroring tools.PreviewStepRequest.
The grammar, and what each action needs:
"click" — selector (or text) "fill" — selector + a NON-EMPTY value "waitFor" — selector (or text); waits for VISIBLE, not merely present "screenshot" — nothing; captures a frame tagged phase "step-<n>"
Text is a SELECTOR shorthand (Playwright's text= engine), never a value alias on fill.
type PreviewStepResult ¶ added in v0.27.0
type PreviewStepResult struct {
Index int `json:"index"`
Action string `json:"action"`
OK bool `json:"ok"`
Skipped string `json:"skipped,omitempty"`
Error string `json:"error,omitempty"`
SettleAfter *PreviewStepSettle `json:"settleAfter,omitempty"`
// DOMText is document.body.innerText after the step.
//
// ⚠️ UNTRUSTED. It is the APP's content, rendered from tenant rows and
// whatever external systems wrote into them. It reaches report.json because
// that is a file the caller chose to write; it is deliberately NOT printed in
// the terminal summary, where it would be indistinguishable from the CLI's
// own words.
DOMText string `json:"domText,omitempty"`
}
PreviewStepResult is what happened at one interaction step.
⚠️ Read Skipped BEFORE OK. A skipped step NEVER RAN (the invocation's step window was spent), so its ok:false says nothing about the app; a step with ok:false and no skipped is one that ran and did not do what it asked.
type PreviewStepSettle ¶ added in v0.27.0
type PreviewStepSettle struct {
Signal string `json:"signal"`
Ms int64 `json:"ms"`
InFlight int `json:"inFlight"`
}
PreviewStepSettle is the abbreviated settle report after a mutating step.
type QueryInput ¶
type QueryInput struct {
SQL string `json:"sql"`
MaxRows int `json:"maxRows,omitempty"`
Format string `json:"format"`
}
QueryInput is the request body.
MaxRows omitted means the server's own default cap; the server also clamps anything above its ceiling, so an over-large value is not an error the CLI has to pre-empt.
type QueryResult ¶
type QueryResult struct {
// Result is the CSV, header row included.
Result string `json:"result"`
RowCount int `json:"rowCount"`
// Truncated reports that the row cap cut the result off, so what is in
// Result is a prefix rather than the answer.
Truncated bool `json:"truncated"`
// Error is the query's own failure — a syntax error, an unresolvable
// {{ ref }}, a permission refusal. Non-empty means the query did not run.
Error string `json:"error"`
SQL string `json:"sql"`
InstanceKind string `json:"instanceKind"`
// ZoneUsed is the IANA reporting timezone the query's DuckDB session ran
// at. The CLI carries no client timezone, so it is normally "UTC" — while
// the same SQL in the browser runs at the signed-in user's zone. Any query
// that buckets by day/week/month can therefore return different numbers on
// the two surfaces, and this is the only field that says so.
ZoneUsed string `json:"zoneUsed"`
}
QueryResult is the csv_meta envelope.
The trap worth stating loudly: a FAILED query comes back as HTTP 200 with Error set. Nothing in the transport layer will flag it, so every caller has to check Error before trusting Result — a query that fails and one that returns no rows are otherwise indistinguishable.
No omitempty anywhere: this struct is emitted verbatim by `ronja query --json`, and a field that vanishes when it is zero makes the payload a different shape depending on the answer.
func (*QueryResult) Failed ¶
func (q *QueryResult) Failed() bool
Failed reports a query the server accepted and could not run.
type RawResponse ¶
type RawResponse struct {
Status int
Header http.Header
Body io.ReadCloser
}
RawResponse is one undecoded HTTP response. The caller owns Body and MUST close it — closing is also what releases the request's deadline.
Header was added back when a caller finally needed it: `ronja api --retry` honours Retry-After, and a retry that ignores the interval the server just named is the one behaviour a rate limiter is entitled to treat as abuse. Still deliberately narrow — everything else about the response is the body.
type RunResponse ¶
type RunResponse struct {
WorkflowRun
// Steps is never null: a legacy or step-less run hydrates as [].
Steps []StepDTO `json:"steps"`
// Health is done|degraded|failed|waiting — see the constants above.
Health string `json:"health"`
// JournalEntries is how many steps this run's resume LINEAGE has journaled
// — what a resume would replay instead of re-running. Derived on read, and
// populated only for a FAILED run: 0 everywhere else, including on a
// successful run that journaled plenty, because that run has nothing to
// resume.
//
// It is NOT len(Steps). Steps are the observable timeline (run spans), a
// different set from the journal, and a report that printed one as the other
// would be a confident wrong number at the moment somebody decides whether
// to trust a resume to skip work.
JournalEntries int `json:"journalEntries"`
}
RunResponse mirrors workflow.RunResponse — the GET /workflow/run/:runID wire shape.
The server embeds *rdb.WorkflowRun, so every run field sits at the JSON ROOT alongside steps and health rather than under a "run" key. The embedded (untagged) WorkflowRun below reproduces exactly that flattening — do not give it a json tag, or the CLI stops decoding every field of the run it polls.
func (*RunResponse) Running ¶
func (r *RunResponse) Running() bool
Running reports a run still in flight — the poll loop's continue condition.
Written as "== running" rather than "not done and not error" on purpose, and durable waits are what turned that from a preference into the only correct spelling: a parked run is in flight in the SERVER's sense — it resumes on its own — but there is nothing here to wait for, because a park lasts until an agent answers or a timer fires, which can be days. "Not done and not error" would sit on it until --timeout.
The same shape covers a status added server-side later, where the failure modes are not symmetric either. Ending the poll on an unrecognised status reports it (a caller sees "status: cancelled" and knows more than the CLI does), while treating it as still-running hangs on a run that has already stopped executing.
type StepDTO ¶
type StepDTO struct {
ID string `json:"id"`
ParentStepID *string `json:"parentStepId"`
Seq int `json:"seq"`
Name string `json:"name"`
// Status is a run-span status; "failed" is the one the health derivation
// keys on.
Status string `json:"status"`
StartedAt string `json:"startedAt"`
EndedAt *string `json:"endedAt"`
DurationMs *int64 `json:"durationMs"`
AICallCount int64 `json:"aiCallCount"`
QueryCount int64 `json:"queryCount"`
TokensUsed int64 `json:"tokensUsed"`
Error *string `json:"error"`
Logs *string `json:"logs"`
// Replayed marks a step whose result came out of the resume lineage's
// journal — the body never ran. Always present server-side, and false for
// every step of a run that is not a resume. It is what separates a
// durability skip from a continue_on_error skip, which share status
// "skipped".
Replayed bool `json:"replayed"`
}
StepDTO mirrors workflow.StepDTO — one observable step of a run, projected from the container-step run spans.
ParentStepID keeps the server's parentStepId spelling (a frontend back-compat name for what is now parent_span_id). TokensUsed is 0 in production today.
type Table ¶
type Table struct {
ID string `json:"id"`
FeatureID string `json:"featureID"`
Name string `json:"name"`
// Kind is derived|foundation|integration|manual_integration|dynamic|metric.
Kind string `json:"kind"`
// Status is pending|invalidated|building|ready|build_failed. Read
// BuildVerdict instead wherever you mean "did the build work" — see the
// constants above.
Status string `json:"status"`
// Code is the table's SQL, in whichever ref form the row happens to store —
// id form from an HTTP write, POSITIONAL form from any AI-assisted build.
// Never hash or write this field raw; use CanonicalCode.
Code string `json:"code"`
InputModels []string `json:"inputModels"`
// ParentModelID is empty on a live table and set on a draft (and on a
// committed version snapshot). ShadowStatus says which.
ParentModelID string `json:"parentModelID"`
// ShadowStatus is draft|committed, empty on a live table.
ShadowStatus string `json:"shadowStatus"`
// BuildVerdict collapses `status` × "is there a failure on THIS ROW?" into
// the one value to branch on after a sync.
//
// Populated ONLY by the single-row GET — never by the list endpoint, and
// never on the create/checkout responses, which share the same DTO. ABSENT
// means "not populated on this response", NOT "no verdict", so read it only
// off a GetTable answer. WaitForTableBuild owns the fallback for an older
// instance that does not send it at all.
BuildVerdict string `json:"buildVerdict,omitempty"`
// LastBuildError is the last recorded failure, absent when there is none.
//
// DRAFT-AWARE, deliberately, and that is not the same scope as BuildVerdict:
// the server reads the table id PLUS the caller's own open draft, so on a
// healthy live table you happen to hold a failed draft of, BuildVerdict is
// `ok` while this describes the draft's failure. The pipeline loop always
// polls the DRAFT's own id, where the two scopes coincide.
LastBuildError *TableBuildError `json:"lastBuildError,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
// URL is the absolute frontend page for this row, stamped by the SERVER.
// Never built here, and EMPTY IS NORMAL AND SILENT — see the identical field
// on Workflow for why. Absent on the list routes and on rows the server
// refuses to link (a committed version snapshot).
URL string `json:"url,omitempty"`
}
Table is a HAND-MIRROR of the subset of feature.TableView the CLI reads (backend/api/v2/feature/tableview.go).
A SUBSET, and a small one: TableView carries some sixty fields — metric recipe columns, build artifacts, PII state, move and soft-delete bookkeeping — and a mirrored field nobody reads is a field nobody keeps in step. The rule is "everything the pipeline loop reads, plus what makes a read field legible".
Mirroring decisions, the same two as Workflow's:
- The server's optional.V[T] marshals as the bare value or JSON `null`, and unmarshalling `null` into a non-pointer is a documented no-op — so every optional STRING here is a plain string. "" is exactly what "unset" means for a name, a parent id or a shadow status.
- InputModels carries no omitempty server-side, but a hand-written fixture or an older instance can still send null, so Normalize replaces the nil.
func (*Table) CanonicalCode ¶ added in v0.27.0
CanonicalCode is Code in ID ref form, plus whether any positional ref could not be resolved.
EVERY remote code read goes through this before it is hashed, written to disk or compared — that is what makes a local sha256 baseline comparable to a remote row at all. See package tablerefs for why the stored form is mixed and why positional is steady state rather than legacy.
func (*Table) IdentityID ¶ added in v0.27.0
IdentityID is the STABLE id of the table this row belongs to: the parent for a draft or a version snapshot, the row itself for a live table.
This is what a manifest binding records, for the reason Workflow.IdentityID exists: a draft's id is per-user and disappears on commit, so a committed file naming one would point at a row that stops existing the moment its author publishes.
type TableBuildError ¶ added in v0.27.0
type TableBuildError struct {
// Message is the human-readable reason. For the commonest derived-table
// failure — the SQL writer concluding the transformation is impossible with
// the given inputs — this carries that full explanation, which is the single
// most useful thing a failed push can print.
Message string `json:"message"`
// Type is the stable rjerr type string. PartialDataCorruptErrorType is the
// non-fatal notice; anything else is an ordinary build failure.
Type string `json:"type,omitempty"`
OccurredAt time.Time `json:"occurredAt"`
}
TableBuildError mirrors feature.TableBuildError: the last failure recorded against a table's build. The rows are cleared at the start of every rebuild, so a present value always describes the LATEST build.
type TableBuildPoll ¶ added in v0.27.0
type TableBuildPoll struct {
// Interval is the wait between the first polls.
Interval time.Duration
// MaxInterval caps the backoff.
MaxInterval time.Duration
// Timeout bounds the whole wait. A non-positive value is DEFAULTED like
// every other field rather than read as "no deadline": there is no caller
// that wants a build watched forever, and a partially-set struct meaning
// exactly that is a hang nobody asked for.
Timeout time.Duration
// MaxFailureWindow bounds an UNBROKEN run of failed polls by ELAPSED TIME.
// Unbroken rather than cumulative: a poll that succeeds proves the instance
// is reachable, so the window starts over.
//
// Time rather than a count, on the device flow's precedent
// (DefaultMaxPollFailureWindow): a count silently means "count x interval",
// so the real tolerance moved whenever the cadence did — three polls at 2.5s
// is ~5 seconds, which is far less than an ordinary rolling deploy takes, and
// a build that is running perfectly well server-side was reported as a lost
// instance.
MaxFailureWindow time.Duration
// RateLimitBackoff is added to the interval on a 429, which is the global
// rate limiter answering. It is the one transient failure where retrying at
// the same cadence is actively wrong: the server is saying there is too much
// traffic, and an unchanged cadence is a promise to keep producing it.
RateLimitBackoff time.Duration
}
TableBuildPoll bounds WaitForTableBuild's cadence and patience.
A struct rather than five more flat fields on Client: the device flow already owns five poll knobs there, and a second flat set would leave every one of them needing its name to say which loop it belongs to.
type TableBuildResult ¶ added in v0.27.0
type TableBuildResult struct {
// Verdict is one of the BuildVerdict* constants. Never empty: an instance
// that does not send one has it derived (see fallbackVerdict).
Verdict string
// Table is the last row read. Never nil on a nil error.
Table *Table
// Derived records that the verdict was computed client-side because the
// instance did not send one — worth knowing before quoting it as the
// server's answer.
Derived bool
}
TableBuildResult is how a build ENDED.
A failed build is DATA here, not an error: the caller renders the reason, leaves the draft in place to iterate on, and exits non-zero itself. An error return from WaitForTableBuild means something else — we stopped being able to ask.
func (*TableBuildResult) ErrorMessage ¶ added in v0.27.0
func (r *TableBuildResult) ErrorMessage() string
ErrorMessage is the reason the build failed, empty when there is none.
func (*TableBuildResult) OK ¶ added in v0.27.0
func (r *TableBuildResult) OK() bool
OK reports a build that produced usable data. `ok_partial` counts: the table IS queryable, and the incompleteness is a warning to print rather than a reason to refuse.
type TableDraftReview ¶ added in v0.27.0
type TableDraftReview struct {
DraftID string `json:"draftID"`
// TableID is the LIVE parent's id.
TableID string `json:"tableID"`
// FieldsDelta is empty when the column sets match. It is only meaningful
// once the draft has been SYNCED: an unbuilt draft has no fields and reports
// every live column as removed.
FieldsDelta []TableFieldDelta `json:"fieldsDelta"`
// InputModelsDelta is the lineage change, ABSENT when the declared inputs
// agree. Reported separately from the SQL because it is the one change a
// draft can make with no SQL diff at all — a positional ref resolves by
// INDEX into this list, so repointing it moves what the same text reads.
InputModelsDelta *TableInputModelsDelta `json:"inputModelsDelta,omitempty"`
// DraftRowCount / LiveRowCount are materialized row counts, or -1 for "not
// available" (never built, no field stats yet). -1 IS NOT ZERO, and printing
// it as one would report a table that was never built as a table that came
// back empty.
DraftRowCount int64 `json:"draftRowCount"`
LiveRowCount int64 `json:"liveRowCount"`
// BaseStale is true when the live table advanced after this draft forked.
// Commit is LAST-WRITER-WINS: landing a stale draft silently reverts those
// changes and nothing on the commit call will stop you, so this plus
// InterveningVersions is the only warning there is.
BaseStale bool `json:"baseStale"`
// InterveningVersions are the versions that landed in between, newest
// first. Present only when BaseStale.
InterveningVersions []TableInterveningVersion `json:"interveningVersions"`
// SubmittedForReview is whether request-review has already been called.
// `pipeline publish` reads it on the review route it takes and reports
// "already submitted" instead of filing a second request: the server accepts
// one and the draft is unharmed, but telling somebody their draft was just
// submitted when an admin has had it since yesterday reports progress that
// did not happen.
SubmittedForReview bool `json:"submittedForReview"`
}
TableDraftReview mirrors governance.TableDraftReview — the diff payload behind a confidence report: what landing this draft would actually change.
Computed entirely from state that already exists (nothing is built or executed), so it is cheap and safe to call in a loop.
⚠️ Note the SCOPE quirk, which is the one thing that can make this call fail on an otherwise working loop: the route lives at /api/v2/table/draft/:id/review in the ADMIN scope group, while the rest of the authoring loop (checkout / PUT / sync / commit) is `data`. A full-access PAT — the CLI default — is unaffected; a token scoped to `data` alone runs the whole loop except this.
type TableFieldDelta ¶ added in v0.27.0
type TableFieldDelta struct {
Name string `json:"name"`
Change string `json:"change"`
DraftType string `json:"draftType,omitempty"`
LiveType string `json:"liveType,omitempty"`
}
TableFieldDelta mirrors governance.TableFieldDelta — one column-level entry in a draft's schema diff.
Change is "added" (only in the draft) or "removed" (only in live). The server compares column NAMES only: "retyped" is RESERVED and currently never emitted, and DraftType / LiveType are reserved with it — currently always absent. Mirrored anyway so the payload decodes if they start arriving; treat an empty one as "not reported" rather than as a type of "".
type TableInputModelsDelta ¶ added in v0.27.0
type TableInputModelsDelta struct {
Added []string `json:"added,omitempty"`
Removed []string `json:"removed,omitempty"`
}
TableInputModelsDelta mirrors governance.TableInputModelsDelta — the LINEAGE change committing a draft would make: the table ids its declared `input_models` gains and loses against the live row's.
Mirrored where the payload's nameDelta and descriptionDelta are NOT, and the rule is the one this file's header states: a mirrored field nobody reads is a field nobody keeps in step. Lineage is the one of the three this loop DECLARES — `inputModels` is derived from the refs in the .sql file and sent on every write — so a delta here is a consequence of the file just pushed and belongs in the confidence report. A name is not: it is stamped once at create and never pushed again.
A SET comparison server-side, so a pure REORDER of the same ids is reported as nothing. That gap is invisible to this loop by construction: the CLI sends a sorted list derived from id-form refs and refuses positional ones outright, which is what makes an order change meaningless here.
type TableInterveningVersion ¶ added in v0.27.0
type TableInterveningVersion struct {
VersionID string `json:"versionID"`
Name string `json:"name"`
CommittedAt time.Time `json:"committedAt"`
}
TableInterveningVersion mirrors governance.TableInterveningVersion — one committed version of the live table that landed AFTER this draft forked.
type TableListItem ¶ added in v0.27.0
type TableListItem struct {
ID string `json:"id"`
FeatureID string `json:"featureID"`
Name string `json:"name"`
Kind string `json:"kind"`
Status string `json:"status"`
// ParentModelID / ShadowStatus are the draft markers. The list EXCLUDES
// shadow rows by default (includeShadows), so in practice both are empty
// here — they are mirrored so a caller can assert that rather than assume it.
ParentModelID string `json:"parentModelID"`
ShadowStatus string `json:"shadowStatus"`
// HasError is the list-only "something is recorded against this row" flag.
// It is a HINT: it says a failure exists, never what it was, and never
// whether the table is nonetheless serving good data. Read the single-row
// GET's buildVerdict for the answer.
HasError bool `json:"hasError"`
UpdatedAt time.Time `json:"updatedAt"`
}
TableListItem is a HAND-MIRROR of the subset of rmodelv2.ModelV2QueryItem the CLI reads (backend/resource/rmodelv2/model.go).
A separate type from Table rather than a reuse, because the LIST projection is a genuinely different shape and pretending otherwise would be a lie in the one direction that costs: it carries NO `code` and NO `buildVerdict`, so a caller handed a Table with both empty could not tell "this table has no SQL" from "this endpoint does not serve SQL". HasError is the list's own cheap health signal and exists on no other surface.
type UpdateTableInput ¶ added in v0.27.0
type UpdateTableInput struct {
Code string `json:"code"`
InputModels []string `json:"inputModels"`
}
UpdateTableInput is the body of PUT /feature/model/:id, mirroring the two fields of rmodelv2.Patch the CLI writes.
BOTH are always sent, together, on every write. This is the single most load-bearing decision in the client, so it is stated here rather than left to a caller to remember:
- Server-side derivation (resolveDerivedRefs) fires ONLY when the effective declared list is empty, and a checked-out draft INHERITS its parent's non-empty input_models. So a PUT that omitted the field would 400 the moment an edit added a new upstream ref — the commonest pipeline edit there is.
- Sending the exact list also REMOVES stale entries. Left behind they are phantom lineage edges that survive in the graph and trigger GetDependents-driven cascade rebuilds of a table that no longer reads from them.
No omitempty on either: an empty `inputModels` is a real declaration ("this table reads from nothing"), and omitempty would collapse it into "derive it for me", which is a different instruction.
type ValidateBindings ¶
type ValidateBindings struct {
InputTableIDs []string `json:"inputTableIDs"`
OutputTableIDs []string `json:"outputTableIDs"`
SecretIDs []string `json:"secretIDs"`
QuerySecretIDs []string `json:"querySecretIDs"`
AgentIDs []string `json:"agentIDs"`
CodexIDs []string `json:"codexIDs"`
}
ValidateBindings mirrors rworkflow.ValidateBindings — what the candidate WOULD bind to if saved as-is. Every slice is always present on the wire (no omitempty server-side), so unlike Workflow's binding columns these need no normalization.
type ValidateFile ¶
ValidateFile is one candidate file, mirroring rworkflow.ValidateFile.
type ValidateFinding ¶
type ValidateFinding struct {
Severity string `json:"severity"`
Code string `json:"code"`
Message string `json:"message"`
Path string `json:"path"`
// Marker is the canonical marker form for the offending binding, when the
// finding is about one. A hint for locating the problem, not necessarily
// the literal text in the file.
Marker string `json:"marker,omitempty"`
}
ValidateFinding mirrors rworkflow.ValidateFinding: one problem, attributed to the source that caused it.
Path is a file path, or a synthetic source ("parameters[0].optionsQuery", "pipPackages", "inputTableIDs") when the problem does not belong to a file — so a client grouping by Path gets a sensible bucket either way.
func (ValidateFinding) IsError ¶
func (f ValidateFinding) IsError() bool
IsError reports whether a finding would block a save.
type ValidateInput ¶
type ValidateInput struct {
FeatureID string `json:"featureID"`
Entrypoint string `json:"entrypoint"`
Parameters []WorkflowParameter `json:"parameters,omitempty"`
Files []ValidateFile `json:"files"`
PipPackages []string `json:"pipPackages,omitempty"`
// RuntimeVersion is 1 or 2 — the runtime the candidate would be checked
// against. 0 sends nothing, which the server reads as 1.
RuntimeVersion int `json:"runtimeVersion,omitempty"`
}
ValidateInput mirrors rworkflow.ValidateFilesInput — a whole candidate workflow, with no workflow ID anywhere in it. That absence is the point: the endpoint persists nothing and needs nothing to exist, which is what makes "iterate an existing script to green before the first push" possible.
Parameters and PipPackages are omitempty because the CLI does not persist either in the manifest today: a workflow's declared parameters live server-side on the draft, and sending an empty list would read as "this candidate declares no parameters" rather than "I am not telling you about them". The server treats an absent list as none, which is the honest answer for a folder that has no record of them.
RuntimeVersion is omitempty for a third reason: the server reads an absent value as 1, so a v1 candidate's request bytes stay exactly what they were before durable workflows existed.
type ValidateResult ¶
type ValidateResult struct {
Findings []ValidateFinding `json:"findings"`
Resolved ValidateBindings `json:"resolved"`
}
ValidateResult mirrors rworkflow.ValidateResult.
func (*ValidateResult) Errors ¶
func (r *ValidateResult) Errors() []ValidateFinding
Errors returns the blocking findings, in the order the server reported them.
func (*ValidateResult) OK ¶
func (r *ValidateResult) OK() bool
OK reports whether the candidate would save cleanly. Warnings do not make it false — a save succeeds through them, and so does a push.
type Workflow ¶
type Workflow struct {
ID string `json:"id"`
WorkspaceID string `json:"workspaceID"`
FeatureID string `json:"featureID"`
Title string `json:"title"`
Description string `json:"description"`
Entrypoint string `json:"entrypoint"`
// Kind is the output-channel discriminator: unspecified|function|report|pipeline.
Kind string `json:"kind"`
// RuntimeVersion is the semantics generation the code is written against: 1
// (the standard runtime) or 2 (durable — steps are journaled, so a failed
// run can be resumed). Stamped at create and afterwards raisable ONE WAY,
// 1 -> 2, through WorkflowPatch; the server refuses 2 -> 1 outright.
//
// ZERO MEANS THE INSTANCE DID NOT SAY, not "runtime 1". The column is NOT
// NULL server-side, so an instance that has it always sends a value; a 0
// here is an instance predating durable workflows, and the caller falls back
// to what the folder's manifest declares rather than concluding v1.
RuntimeVersion int `json:"runtimeVersion"`
Parameters []WorkflowParameter `json:"parameters"`
Variables map[string]any `json:"variables"`
// ReportingTimezone is the IANA calendar the workflow's DuckDB session runs
// at on EVERY run (migration 000499) — what date_trunc and date casts bucket
// against, so it decides which day or month a timestamp near midnight lands
// in. A plain string for the reason the other optional strings are: the
// server's optional.V marshals `null` when unset, and unmarshalling null into
// a string is a documented no-op.
//
// "" therefore means the row declares NO zone, and falls back to the caller's
// context zone at run time. That is a PERMANENT state, not a migration
// backlog: 000499 backfills nothing, and a workflow created while the
// organization has no default declares nothing either. It is NOT the same as
// the literal "UTC" a reset writes, and the CLI keeps the two apart.
ReportingTimezone string `json:"reportingTimezone"`
UseDedicatedCompute bool `json:"useDedicatedCompute"`
// ApprovalGate is null on a workflow with no gate. `wf test` (Phase 4)
// preflights Enabled: a gated workflow cannot be run over HTTP at all,
// because only an agent session can produce an approval.
ApprovalGate *WorkflowApprovalGate `json:"approvalGate"`
// ParentWorkflowID is empty for a live row AND for a parentless draft (a
// brand-new workflow nobody has published yet). Non-empty on an edit draft
// and on a committed version snapshot.
ParentWorkflowID string `json:"parentWorkflowID"`
CommittedAt *time.Time `json:"committedAt"`
Hidden bool `json:"hidden"`
SecretIDs []string `json:"secretIDs"`
ExplicitSecretIDs []string `json:"explicitSecretIDs"`
InputTableIDs []string `json:"inputTableIDs"`
OutputTableIDs []string `json:"outputTableIDs"`
AgentIDs []string `json:"agentIDs"`
CodexIDs []string `json:"codexIDs"`
QuerySecretIDs []string `json:"querySecretIDs"`
PipPackages []string `json:"pipPackages"`
// Lifecycle is trigger-computed server-side: live|draft|version|archived|proposed.
Lifecycle string `json:"lifecycle"`
// SubmittedForReviewAt is set once a drafter asks an admin to commit their
// draft; null on every other row, and on a draft they have not submitted.
SubmittedForReviewAt *time.Time `json:"submittedForReviewAt"`
DrafterUserID string `json:"drafterUserID"`
// FeatureScope is the joined feature's scope (private|workspace|
// organization), stamped at read time — the field to branch on for privacy,
// not the legacy scope/private columns.
FeatureScope string `json:"featureScope"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// URL is the absolute frontend page for this row, stamped by the SERVER
// (api/v2/workflow.WorkflowView) on the single-row routes. Never built here:
// the instance URL a profile records is the API origin, and the deploy
// template puts the backend on api.* and the frontend on app.*, so a link
// assembled from it points at a host that serves no pages. In local dev the
// two differ even more sharply.
//
// EMPTY IS NORMAL AND SILENT. The field is omitempty server-side and an
// instance with no configured frontend origin emits none at all (see
// backend/lib/deeplink), which is the ordinary state of a dev box. A caller
// with no url prints no link — it never falls back to deriving one, and it
// never treats absence as an error.
//
// Absent on the LIST routes (GET /workflow/query, GET :id/versions) and on
// rows the server refuses to link, so read it off the response you have
// rather than assuming every Workflow value carries one.
URL string `json:"url,omitempty"`
}
Workflow is a HAND-MIRROR of the subset of rdb.Workflow the CLI reads.
Same rule as Me in auth.go, and the same hazard: a json tag that does not exist on the wire decodes to the zero value SILENTLY, so a typo here shows up as an empty column of output or — worse — as "no approval gate" on a workflow that has one. Every tag below is copied from backend/ronja/rdb/table_workflow.go; keep them in step with that file.
Two mirroring decisions worth knowing about:
- The server's optional.V[T] marshals as the bare value or JSON `null`. Unmarshalling `null` into a non-pointer is a documented no-op in encoding/json, so optional STRINGS are plain strings here ("" is exactly what "unset" means for an id) while optional TIMESTAMPS are pointers, because "is there a submitted-for-review stamp at all" is a question the CLI genuinely asks.
- The ID slices carry `omitempty` server-side, so an empty binding set is ABSENT from the JSON rather than `[]`. Normalize() replaces the resulting nils with empty slices so callers can range and compare without nil-checking every field.
This is a SUBSET, not the whole row: rdb.Workflow carries roughly twenty more columns (schedule and run bookkeeping, logic summaries, governance stamps) that no CLI command reads. Adding a field here is cheap and adding one that nothing consumes is dead weight nobody keeps in step — so the rule is "everything the CLI reads, plus what makes a read field legible", and the fixture test in workflow_test.go is what checks those tags against the wire.
func (*Workflow) IdentityID ¶
IdentityID is the STABLE id of the workflow this row belongs to: the parent for an edit draft or a version snapshot, the row itself for a live workflow or a parentless draft.
This is what a manifest binding records. A draft's own id is per-user and disappears on commit, so a committed file naming one would point at a row that stops existing the moment its author publishes.
type WorkflowApprovalGate ¶
type WorkflowApprovalGate struct {
Enabled bool `json:"enabled"`
}
WorkflowApprovalGate is a SUBSET of rdb.WorkflowApprovalGate: what the CLI acts on, which is whether the gate is switched on at all.
The rest of the server's shape — the approvers, the author's message, the context parameter names, and the whole email/Slack delivery tree — is deliberately NOT mirrored. A gate the CLI cannot run is a gate the CLI has nothing to say about, and a mirrored field nobody reads is a field nobody keeps in step with backend/ronja/rdb/table_workflow.go. Add them back when a command genuinely needs them (a `wf status` that names the approvers and says where the request goes is the obvious candidate), not before.
type WorkflowFile ¶
type WorkflowFile struct {
ID string `json:"id"`
WorkflowID string `json:"workflowID"`
Path string `json:"path"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
WorkflowFile mirrors rdb.WorkflowFile — one source file of one workflow row. There is no content hash on the wire, so the CLI hashes Content itself.
type WorkflowParameter ¶
type WorkflowParameter struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
Type string `json:"type"` // "string" | "number" | "date" | "select"
DefaultValue any `json:"defaultValue,omitempty"`
Required bool `json:"required"`
Options []string `json:"options,omitempty"`
OptionsQuery string `json:"optionsQuery,omitempty"`
}
WorkflowParameter mirrors rdb.WorkflowParameter. OptionsQuery is SQL carrying `{{ ref('tableID') }}` markers, which is why validation attributes findings from it to a synthetic parameters[N].optionsQuery path.
type WorkflowPatch ¶
type WorkflowPatch struct {
Title string `json:"title,omitempty"`
Entrypoint string `json:"entrypoint,omitempty"`
Parameters *[]WorkflowParameter `json:"parameters,omitempty"`
ReportingTimezone *string `json:"reportingTimezone,omitempty"`
RuntimeVersion int `json:"runtimeVersion,omitempty"`
}
WorkflowPatch is the body of PUT /workflow/:id, mirroring the subset of rworkflow.Patch the CLI writes.
Two fields, both omitempty: every field of the server's Patch is an optional.V, so an absent key means "unchanged" while an explicit empty string would CLEAR the value. A folder whose manifest has lost its title must not be able to blank the workflow's.
Entrypoint is here because the manifest owns it: without this the row keeps running whatever file it was created with, and an entrypoint RENAME is impossible — the server refuses to delete the file the row still names. Ordering matters and belongs to the caller: rworkflow's Update refuses an entrypoint the workflow has no file for, so the new file must be written BEFORE this patch, and the old one deleted after it. Parameters is a POINTER for the reason the string fields are omitempty: the server's Patch.Parameters is an optional slice, so an absent key leaves the declaration alone while an explicit [] CLEARS it. Both are things a folder legitimately means — "I don't manage parameters" and "I declare none" — and a plain slice with omitempty collapses them into the first, making it impossible to remove the last parameter from a workflow. ReportingTimezone is a POINTER for the same reason Parameters is one, and the server's rule is what forces it: an absent key leaves the declared execution zone alone, while an explicit "" RESETS it to the literal "UTC" (rworkflow.resolveDeclaredZonePatch). Both are things a folder legitimately means — "I don't manage the zone" and "I declare UTC" — and a plain string with omitempty collapses them into the first, making the reset unexpressible. RuntimeVersion is a plain int with omitempty, and the third state the two pointers carry has nothing to describe here: the field moves ONE WAY (1 -> 2, the Durable runtime), so "the folder does not manage it" and "the folder declares the runtime the row already has" both mean "send nothing". The server refuses 2 -> 1 outright; the CLI refuses it earlier, before it has written anything.
func (WorkflowPatch) Empty ¶
func (p WorkflowPatch) Empty() bool
Empty reports a patch that would change nothing, so the caller can skip the round trip rather than send `{}`.
type WorkflowRun ¶
type WorkflowRun struct {
ID string `json:"id"`
WorkflowID string `json:"workflowID"`
ParameterValues map[string]any `json:"parameterValues"`
// Status is running|done|error|waiting|resuming — see the constants above.
Status string `json:"status"`
Outputs []WorkflowRunOutput `json:"outputs"`
TableOutputs []WorkflowRunTableOutput `json:"tableOutputs"`
// Logs is the run's FULL captured output, on the row itself. There is no
// streaming endpoint, which is why the CLI tails this at the end rather
// than following it live.
Logs string `json:"logs"`
Error *string `json:"error"`
ExecutedBy string `json:"executedBy"`
ExecutedAt time.Time `json:"executedAt"`
CompletedAt *time.Time `json:"completedAt"`
// ExecRunID / TraceID link the run to the generic execution telemetry and
// to the cross-primitive trace. Both nullable; the CLI carries them so a
// caller can follow either one into the web UI.
ExecRunID *string `json:"execRunID"`
TraceID *string `json:"traceID"`
// TriggeringRunKind / TriggeringRunID name the run that triggered this one.
// Always absent for a CLI-started run — it is a top-level execution — and
// mirrored anyway so the shape is the server's, not a CLI-shaped subset.
TriggeringRunKind *string `json:"triggeringRunKind"`
TriggeringRunID *string `json:"triggeringRunID"`
// ProcessingStartedAt is stamped by the Python harness the moment user code
// begins, so the gap from ExecutedAt is queue + container start. Absent when
// the container never reported in.
ProcessingStartedAt *time.Time `json:"processingStartedAt"`
// ResumeOfRunID marks this run as a RESUME of an earlier failed one: the
// lineage's journaled step results were replayed instead of re-executed.
// Absent for an ordinary run, which is every run of a v1 workflow.
//
// It always names the LINEAGE ROOT, never the run the caller asked to
// resume — the server normalizes it — so a chain R1 → R2 → R3 has both R2
// and R3 pointing at R1. Nothing in the CLI has to unwind that: a resume is
// requested against the failed run you have, and the server does the rest.
ResumeOfRunID *string `json:"resumeOfRunID"`
// Kind is the workflow's output channel, denormalized at run-create time:
// unspecified|function|report|pipeline.
Kind string `json:"kind"`
// RuntimeVersion is the workflow's runtime, denormalized at run-create time
// — the semantics this run and every resume of its lineage execute under,
// which is deliberately not re-read off the workflow row. 0 on an instance
// that predates durable workflows.
RuntimeVersion int `json:"runtimeVersion"`
// Body is the inline return value of a function-kind run, capped server-side
// at 8 KB.
Body *string `json:"body"`
// Telemetry counters, hydrated from the joined exec_run. Zero for legacy
// runs and for runs that made no AI call or query.
TokensUsed int64 `json:"tokensUsed"`
AICallCount int64 `json:"aiCallCount"`
QueryCount int64 `json:"queryCount"`
}
WorkflowRun mirrors rdb.WorkflowRun — one execution of one workflow row.
Mirrored in FULL rather than as a subset (unlike Workflow), because this struct is what `--json` prints: the CLI reads maybe six of these fields, but dropping the rest would quietly narrow the machine-readable output of a command whose whole purpose is to report what a run did.
The optional.V columns are pointers wherever "absent" is a real answer (a run still going has no completedAt; a successful one has no error). TenantID is deliberately not mirrored: it is the server's bookkeeping, and nothing at the command line has a use for it.
type WorkflowRunOutput ¶
type WorkflowRunOutput struct {
FileKey string `json:"fileKey"`
// Format is html|csv|xlsx|json|pdf|docx|xml.
Format string `json:"format"`
FileID string `json:"fileID,omitempty"`
// Name is the author-supplied filename, empty for unnamed outputs.
Name string `json:"name,omitempty"`
}
WorkflowRunOutput mirrors rdb.WorkflowRunOutput — one file a run produced.
type WorkflowRunTableOutput ¶
type WorkflowRunTableOutput struct {
ModelID string `json:"modelID"`
LogicalName string `json:"logicalName"`
DisplayName string `json:"displayName"`
// WriteMode is replace|append.
WriteMode string `json:"writeMode"`
FileCount int `json:"fileCount"`
}
WorkflowRunTableOutput mirrors rdb.WorkflowRunTableOutput — one TABLE a run wrote to. This is the thing `--write-live` exists for: WriteMode "replace" means the run replaced the table's contents.