Build apps
Using data and Workflows
How an app uses the SDK to read and write records, confirm values, import CSV files, start and inspect cases, and complete and approve Tasks.
This page shows code for using business data, Workflows, and Tasks from your app’s BFF. In the examples, hane is an SDK client created with the user’s access token (SDK). The concepts are covered in Data, Workflows, and Approvals.
Read records
Section titled “Read records”// types and fields in the active release. Take field labels on screens from titleconst { types } = await unwrap(hane.GET("/v1/data/types"));
// contracts, each with its current relationsconst contracts = await unwrap( hane.GET("/v1/data/records", { params: { query: { type: "LeaseContract", include: "relations", limit: 500 } } }),);
// only contracts whose end date is still estimatedconst unconfirmed = await unwrap( hane.GET("/v1/data/records", { params: { query: { type: "LeaseContract", field: "end_date", state: "estimated" } } }),);Each field of a record has a value, a state (the value state), and a source. Show the value state next to the value on screens so that unconfirmed values are easy to spot.
const { entity } = await unwrap(hane.GET("/v1/data/records/{id}", { params: { path: { id } } }));const end = entity.values.end_date; // { value: "2027-03-31", state: "estimated", source: { kind: "computed", ... } }Write records
Section titled “Write records”// create. Each value is { value, state?, note? }await unwrap(hane.POST("/v1/data/records", { body: { type: "Vehicle", values: { registration_no: { value: "TEST 300 A 10-01" } } } }));
// update. Send the version you read in If-Match, and record a reasonawait unwrap( hane.PATCH("/v1/data/records/{id}", { params: { path: { id }, header: { "If-Match": ifMatch(entity.version) } }, body: { values: { start_date: { value: "2024-04-01" } }, reason: "Checked the start date against the vehicle registration" }, }),);
// create a relationawait unwrap(hane.POST("/v1/data/relations", { body: { type: "covers", from: contractId, to: vehicleId } }));Don’t write fields that a rule computes (such as the end date) directly; update their input fields instead. Hane recalculates the value and its value state.
Confirm a value
Section titled “Confirm a value”When a person has checked a value against a document, raise it to confirmed. Only a person can do this, so call it with the user’s token. The user is recorded as the person who confirmed it.
await unwrap( hane.POST("/v1/data/records/{id}/values/{field}/confirm", { params: { path: { id, field: "start_date" } }, body: { note: "Checked on page 1 of the contract" }, }),);const history = await unwrap( hane.GET("/v1/data/records/{id}/values/{field}/history", { params: { path: { id, field: "start_date" } } }),);Import a CSV
Section titled “Import a CSV”Use field names as headers. A <field>@state column sets the value state, and an @rel:<relation> column sets the key of the related record. Map your own ledger’s columns to Hane fields in your app before sending.
const result = await unwrap( hane.POST("/v1/data/imports", { body: { type: "Vehicle", csv, label: "Vehicle ledger 2026-10", default_state: "estimated" }, }),);console.log(result.rows, result.created, result.updated, result.failed);You can also import with hanectl: hanectl import Vehicle -f vehicles.csv --state estimated.
Start a case
Section titled “Start a case”To start a case from your app, use one of the following. The public API has no endpoint that creates a case directly.
| How | What to write |
|---|---|
| A daily schedule | A Schedule with forEach creates a case for each record that matches the condition |
| From a screen or the chat | Run an agent that has the workflow.start tool as an AgentRun |
To start a case from a button on a screen, define a scripted agent that has only workflow.start, and call it from the BFF.
apiVersion: agents/v1alpha1kind: Agentmetadata: name: procurement-startspec: purpose: Start a procurement case from a replacement or additional vehicle request input: type: object required: [request_no] properties: request_no: { type: string, title: Procurement number, pattern: "^P-[0-9]{4}$" } driver: scripted tools: [workflow.start] workflows: [procurement] limits: { maxToolCalls: 1, maxSeconds: 60 } output: type: object required: [status, case_id] properties: status: title: Result oneOf: - { const: started, title: Started the case } case_id: { title: Case, type: string } script: - call: workflow.start args: { workflow: '"procurement"', key: input.request_no } as: started - submit: status: '"started"' case_id: steps.started.case.case_idconst run = await unwrap(hane.POST("/v1/agents/runs", { body: { agent: "procurement-start", input: { request_no: "P-1001" } } }));To trigger a schedule right now with today’s date, use POST /v1/workflows/schedules/{name}/trigger (hanectl schedules trigger <name>).
Read cases
Section titled “Read cases”const { items } = await unwrap(hane.GET("/v1/workflows/cases", { params: { query: { workflow: "lease-expiry", status: "waiting" } } }));
// history, waits, and the Workflow definition the case started withconst detail = await unwrap(hane.GET("/v1/workflows/cases/{id}", { params: { path: { id: caseId } } }));console.log(detail.case.current_step, detail.case.attention, detail.waits);
// the case's Tasksconst tasks = await unwrap(hane.GET("/v1/approvals/tasks", { params: { query: { case_id: caseId } } }));case.attention lists values that are not confirmed (estimated, pending inquiry, and so on). On a case screen, show them as values to check before deciding. To cancel a case, call POST /v1/workflows/cases/{id}/cancel with a reason.
Decide a Task
Section titled “Decide a Task”A Workflow task step creates a Task for the group or assignee from an assignment rule. When the user closes the Task with an outcome, the case moves on.
const mine = await unwrap(hane.GET("/v1/approvals/tasks", { params: { query: { mine: true, status: "open" } } }));const task = mine.items[0];
await unwrap( hane.POST("/v1/approvals/tasks/{id}/complete", { params: { path: { id: task.task_id }, header: { "If-Match": ifMatch(task.version) } }, body: { outcome: "renew", comment: "Terms are unchanged, so we renew the lease" }, }),);outcome must be one of the keys of the Task’s outcomes. Use the values of outcomes as display labels.
Approve an external action
Section titled “Approve an external action”External actions that an agent proposes, such as email, SMS, or calls (ActionRequests), become approval Tasks. On the approval screen, read the ActionRequest linked to the Task, show its content, and send the digest of what the user saw. If the content has changed, the request fails with 409 and neither approval nor rejection is recorded.
const { items } = await unwrap( hane.GET("/v1/agents/action-requests", { params: { query: { approval_task_id: task.task_id, limit: 1 } } }),);const action = items[0]; // tool, parameters (recipients are masked), action_digest
await unwrap( hane.POST("/v1/approvals/tasks/{id}/complete", { params: { path: { id: task.task_id }, header: { "If-Match": ifMatch(task.version) } }, body: { outcome: "approve", action_digest: action.action_digest }, }),);