> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getcliq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial — your first hug

> Build review-batch: a team hug that asks reviewers for a verdict on each document, used from agent code and from a model.

Hugs

You'll build **`review-batch`**: a page that shows a company's documents, each with its file, asks for **OK** or
**Not OK** on each one with a note saying why, and takes optional evidence. A custom agent and an LLM phase both ask
with it. The finished code is `examples/hugs/review-batch` in the cliq repo.

About 30 minutes. You need cliq with cliqd, Node 22, and a team folder (`team.yml`).

Blocks marked *(abridged)* are shortened for reading; the full files are in [`examples/hugs/review-batch`](https://github.com/getcliqio/cliq/tree/main/examples/hugs/review-batch).

***

## 1. Decide whether you need a hug

`form` can ask "Is document 1 OK? Is document 2 OK? Notes?" — but a reviewer then matches fields to files by hand,
and nothing says a rejection needs a note. A hug made for the job shows each document next to its verdict, asks for a
note only where one is needed, and refuses an answer without one. That's worth a hug.

## 2. Scaffold it

A hug is built with `@getcliqio/hug-frontend`, which comes from GitHub Packages like `@getcliqio/cliq-sdk`. If you
haven't set that up yet (once per machine; a GitHub token with `read:packages`):

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> ~/.npmrc
echo "@getcliqio:registry=https://npm.pkg.github.com" >> ~/.npmrc
```

In the team folder:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq hug new review-batch
cd hugs/review-batch && npm install
```

`cliq hug new` writes `hugs/review-batch/` (inside a team folder; elsewhere, `review-batch/`) and doesn't touch
`team.yml`:

| File | |
| - | - |
| `hug.ts` | the definition |
| `src/main.ts` | the page (`--react` for `src/main.tsx` with React) |
| `hug.test.ts` | a test of the definition |
| `build.mjs`, `package.json`, `tsconfig.json` | its build — yours to change |

## 3. Write the definition

`hug.ts` says what the hug is asked and what an answer is, with [Zod](https://zod.dev). Both the page and the agent
import it.

```ts hugs/review-batch/hug.ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { define_hug, file } from '@getcliqio/hug-frontend/define';
import { z } from 'zod';

export default define_hug({
    name: 'review-batch',
    title: 'Review a batch of documents',
    description:
        'Asks a reviewer to look at a batch of a company’s documents and give each one a verdict: OK, or not OK with a ' +
        'note saying why. Params: the company, and the documents (id, name, and the artifact_id of the attached file). ' +
        'The answer has one verdict per document, in order, and optional evidence.',
    params: z.object({
        company: z.string().min(1).describe('Company'),
        documents: z.array(z.object({
            id: z.string().min(1),
            name: z.string().min(1),
            artifact_id: z.string().optional().describe('The attached file to show'),
        })).min(1).max(50),
    }),
    values: (p) => z.object({
        verdicts: z.array(z.object({
            id: z.string(),
            ok: z.boolean().describe('OK'),
            note: z.string().trim().max(500).optional().describe('Why not'),
        })).length(p.documents.length, 'one verdict per document').superRefine((verdicts, ctx) => {
            verdicts.forEach((v, i) => {
                if (v.id !== p.documents[i]?.id) ctx.addIssue({ code: 'custom', path: [i, 'id'], message: 'verdicts follow the documents, in order' });
                if (!v.ok && !v.note) ctx.addIssue({ code: 'custom', path: [i, 'note'], message: 'Say what is wrong with this document' });
            });
        }).describe('Verdicts'),
        evidence: file({ accept: ['image/*', '.pdf', '.txt'], optional: true, max_bytes: 5_000_000 }).describe('Evidence'),
    }),
    summary: (v) => {
        const ok = v.verdicts.filter((x) => x.ok).length;
        return ok === v.verdicts.length ? `All ${ok} OK` : `${ok} of ${v.verdicts.length} OK`;
    },
});
```

* **`values` is a function of the params** here: the answer needs exactly one verdict per document asked about.
* **`file()`** makes `evidence` an upload slot — the person picks a file, and the answer holds a reference to it.
* **`summary`** is the one line shown in the inbox and notifications.
* **`description`** is for models: an LLM phase that asks with this hug is told it ([step 9](#9-use-it)).
* **`.describe()`** gives a field its label in the default read-only view.

## 4. Write the page

`mount_hug` connects to the Hub page, keeps the answer, checks it with the definition on every change and redraws.
You write the editor:

```ts hugs/review-batch/src/main.ts (abridged) theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { html, mount_hug, nothing } from '@getcliqio/hug-frontend';
import '@getcliqio/hug-frontend/ui';
import review_batch from '../hug';

const CHOICES = [{ value: 'ok', label: 'OK' }, { value: 'no', label: 'Not OK', tone: 'danger' }];

mount_hug(review_batch, {
    initial: (p) => ({ verdicts: p.documents.map((d) => ({ id: d.id, ok: true })) }),
    submit: (v) => (v.verdicts.every((x) => x.ok) ? 'Approve all' : 'Send verdicts'),
    edit: ({ params, value, set, problems, hug }) => html`
        <p>Documents for <strong>${params.company}</strong></p>
        ${params.documents.map((d, i) => {
            const v = value.verdicts[i];
            const file = hug.attachments.find((a) => a.artifact_id === d.artifact_id);
            return html`<div class="doc">
                <h3>${d.name}</h3>
                ${file ? html`<cliq-attachment .ref=${file}></cliq-attachment>` : nothing}
                <cliq-choice-group label="Verdict" .value=${v.ok ? 'ok' : 'no'} .choices=${CHOICES}
                    @change=${(e) => {
                        set(`verdicts[${i}].ok`, e.detail === 'ok');
                        if (e.detail === 'ok') set(`verdicts[${i}].note`, undefined);
                    }}></cliq-choice-group>
                ${v.ok ? nothing : html`<cliq-field label="What is wrong?" multiline required
                    .value=${v.note} .problem=${problems[`verdicts[${i}].note`]}
                    @change=${(e) => set(`verdicts[${i}].note`, e.detail)}></cliq-field>`}
            </div>`;
        })}
        <cliq-file key="evidence" label="Evidence (optional)" .value=${value.evidence}
            @change=${(e) => set('evidence', e.detail)}></cliq-file>`,
});
```

* **`initial`** — the answer before the person does anything: every document OK.
* **`submit`** — the Hub's Submit button's label.
* **`edit`** — draws the editor; `set(path, value)` changes one part of the answer, and the page redraws.
* **`problems`** — the definition's problems by path, so "Say what is wrong with this document" appears under the
  note it belongs to.
* The **UI kit** (`<cliq-field>`, `<cliq-choice-group>`, `<cliq-file>`, `<cliq-attachment>`) looks like the Hub,
  in light and dark, and is accessible. See [UI kit](https://docs.getcliq.io/hugs/ui-kit.md).

## 5. Work on it in the browser

Make a `params.json` with sample params:

```json params.json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{ "company": "ACME", "documents": [{ "id": "lease", "name": "Lease" }, { "id": "payslip", "name": "Payslip" }] }
```

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq hug dev . --params params.json
```

`cliq hug dev` builds the hug, shows it on cliqd's local Hug page — no run, no Hug — and rebuilds and reloads as you
edit. A side panel shows what the page sends: the values, whether they're valid, the problems, the Submit label and
the summary. Pick **Not OK** on a document and leave the note empty: Submit stays disabled and the note shows why.

## 6. See the read-only view

After someone answers, the page draws the answer read-only. `mount_hug` draws a default view from the definition
(labels from `.describe()`); check it with a sample answer:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq hug dev . --params params.json --view answer.json
```

To draw your own, add `view`:

```ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    view: (v, { params }) => html`
        ${v.verdicts.map((x, i) => html`<div>
            ${x.ok ? '✓' : '✗'} <strong>${params.documents[i]?.name ?? x.id}</strong>${x.note ? html` — ${x.note}` : nothing}
        </div>`)}
        ${v.evidence ? html`<p>Evidence: <cliq-attachment .ref=${v.evidence}></cliq-attachment></p>` : nothing}`,
```

## 7. Show the documents and take evidence

The documents reach the page as **attachments** — run files the agent attaches when it asks (step 9). In `cliq hug
dev`, give it a folder of files to stand in for them:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq hug dev . --params params.json --attachments ../../sample/
```

`cliq hug dev` gives the folder's files the ids `dev-file-0`, `dev-file-1`, … in name order, so point the sample
params at them — `{ "id": "lease", "name": "Lease", "artifact_id": "dev-file-0" }` — and each document shows its file.

`<cliq-attachment>` shows each file with **Preview** (text, markdown and images) and **Download**. `<cliq-file
key="evidence">` asks the Hub to let the person pick a file and upload it — the page itself never touches the network.

## 8. Test the definition

The definition is plain code: test it like any.

```ts hugs/review-batch/hug.test.ts (abridged) theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import { describe, expect, it } from 'vitest';
import review_batch from './hug';

const PARAMS = { company: 'ACME', documents: [{ id: 'lease', name: 'Lease' }, { id: 'payslip', name: 'Payslip' }] };

describe('review-batch', () => {
    it('a rejection needs a note', () => {
        const p = review_batch.check_params(PARAMS);
        if (!p.ok) throw new Error('params');
        const missing = review_batch.check({ verdicts: [{ id: 'lease', ok: true }, { id: 'payslip', ok: false }] }, p.value);
        expect(missing).toMatchObject({ ok: false, problems: [{ path: 'verdicts[1].note' }] });
    });
});
```

To test the page itself, `mock_host` from `@getcliqio/hug-frontend/testing` stands in for the Hub — see
[Testing hugs](https://docs.getcliq.io/hugs/testing.md).

## 9. Use it

Build it, then name it in the team:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq hug build .
```

**From agent code** — a custom agent attaches the documents and asks by name:

```js agents/batch-reviewer/agent.mjs (abridged) theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
const attachments = await ctx.attach(['documents/*']);
const documents = attachments.map((a) => ({ id: id_of(a.name), name: a.name, artifact_id: a.artifact_id }));

const answer = await ctx.ask('review-batch', {
    title: `Review ${company}'s documents`,
    params: { company, documents },
    attachments,
});
// Already checked against the definition: one verdict per document, a note for each one that isn't OK.
for (const v of answer.value.verdicts) console.log(v.id, v.ok ? 'OK' : `not OK: ${v.note}`);
```

**From a model** — an LLM phase that names the hug is told its title, description and params, and asks with it when
its role says to:

```yaml team.yml (abridged) theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
phases:
  - name: review
    agent: batch-reviewer
    hug:
      name: review-batch
      channels: ["{{inputs.reviewers}}"]
      timeout: 2h

  - name: follow-up
    agent: claude-api
    role: roles/follow-up.md
    depends_on: [review]
    hug:
      name: review-batch
      channels: ["{{inputs.reviewers}}"]
```

Run the team locally:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq run -i reviewers=me
```

`cliq run` opens the Hug's page in your browser. Answer it — then try sending **Not OK** without a note through
cliqd's API ([answering without a browser](https://docs.getcliq.io/hugs.md#on-your-machine)): the agent doesn't count it, the page shows "1
answer couldn't be used; waiting for a valid one", and the Hug stays open.

## 10. Publish it with the team

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
cliq team publish
```

The team's hugs travel with it — only their built `dist/`. `cliq team publish` refuses a hug whose `dist/` is missing
or out of date (`--build` builds first). Run the team on a CliqHub realm: the first Hug asked with `review-batch`
uploads its bundle to CliqHub, and every later one with the same build sends nothing. Change the hug, rebuild,
publish a new version: new Hugs use the new bundle, open ones keep theirs.

***

## Next steps

* **More examples:** [`examples/hugs`](https://github.com/getcliqio/cliq/tree/main/examples/hugs) — lists, side-by-side files, image markup, checklists ([all of them](https://docs.getcliq.io/hugs/overview.md#examples)).
* **React:** `cliq hug new review-batch --react`, and [`review-batch-react`](https://github.com/getcliqio/cliq/tree/main/examples/hugs/review-batch-react) — `useHug`, `<HugRoot>` ([React](https://docs.getcliq.io/hugs/react.md)).
* **Several reviewers:** a `groups` policy on `ctx.ask` ([Asking people](https://docs.getcliq.io/hugs.md#example-5-several-reviewers-and-letting-the-agent-stop-while-waiting)).
* **A hug for several teams:** keep it in a folder of your own on `CLIQ_HUGS_PATH` ([Installing](https://docs.getcliq.io/hugs/installing.md)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.