CRUMB a card from devarno-cloud

Job Schema & Audit Metadata

so1 intermediate 5 min read

ELI5

A job is a delivery slip: the parcel itself (output) plus a stamped audit slip (metadata) that says who shipped it (userId), from which depot (orgId), the manifest number (requestId), what was inside (inputs, with sensitive items blacked out), and a running courier note (logs).

Technical Deep Dive

Zod Schema

Defined in so1-shared/src/jobs.ts:

classDiagram
class Job {
+id : string (uuid)
+state : JobState
+createdAt : iso datetime
+startedAt? : iso datetime
+completedAt? : iso datetime
+output? : unknown
+error? : ErrorDetail
+logs : string[] (default [])
+metadata : JobMetadata
}
class JobMetadata {
+requestId : string (uuid)
+userId : string
+orgId : string
+action : string
+inputs : unknown
}
class ErrorDetail {
+code : string
+message : string
}
class JobStatus {
+requestId : string (uuid)
+job : Job
}
Job --> JobMetadata
Job --> ErrorDetail
JobStatus --> Job

Field Notes

FieldConstraintSource
idz.string().uuid()server-generated on create
statez.nativeEnum(JobState)see so1-006
logsz.array(z.string()).default([])append-only
metadata.requestIdz.string().uuid()matches the inbound request’s id (so1-012)
metadata.userIdz.string()from auth context
metadata.orgIdz.string()from auth context
metadata.inputsz.unknown()passed through redactInputs (ADR-003) before storage

Redaction Contract

Per ADR-003, before any inputs reach durable storage:

function redactInputs(action: string, inputs: unknown): unknown {
if (action === "trigger-github-workflow") {
return { repo: inputs.repo, branch: inputs.branch }; // pat/token omitted
}
return inputs;
}

The list of redactable fields is action-specific and lives next to the action handler.

JobStatus vs Job

JobStatus wraps a Job with the top-level requestId of the current status request (not the original create request) so each GET /api/jobs/{id} call is independently traceable.

Key Terms

  • Audit metadatawho/what/when/why fields preserved beyond the job’s lifetime.
  • Redaction → removing or masking fields whose values must not be persisted (tokens, keys).
  • append-only logs → log array can only grow; entries are never edited or removed.

Q&A

Q: Why is inputs typed unknown rather than per-action union? A: Job actions are open-ended; per-action input schemas live with each action handler, which validates before redaction. The Job model stays generic.

Q: Are startedAt and completedAt ever both unset? A: Yes — a job in pending has neither.

Q: Is logs part of the create response? A: It defaults to [] — present but empty until the worker emits lines.

Examples

A successful workflow trigger ends with { state: "success", startedAt: "2026-03-01T10:00:00Z", completedAt: "2026-03-01T10:00:42Z", output: { runId: 7700123 }, metadata: { action: "trigger-github-workflow", inputs: { repo: "org/x", branch: "main" } } } — the PAT used to call GitHub never appears.

neighbors on the map