---
summary: "Create a scope, create a queue, publish a message, claim it, complete it\
  \ \u2014 five minutes end-to-end."
title: Quickstart
path: quickstart
status: published
---

In five minutes you'll have a working scope, a queue inside it, one message published, one message claimed, and one message completed. This is the smallest possible useful ScaiQueue interaction.

You need:

- A ScaiGrid API key with `scaiqueue:manage`, `scaiqueue:publish`, and `scaiqueue:consume` (any tenant admin has all three).
- `curl`, Python with `httpx`, or Node — pick one of the blocks below.

```bash
export SCAIGRID_HOST="https://scaigrid.scailabs.ai"
export SCAIGRID_API_KEY="sgk_..."
```

## 1. Create a scope

A scope is a namespace for related queues. Creating one also auto-creates the system queues (`_dead_letter`, `_events`, `_audit`, `_integrity`, `_corrections`).

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "quickstart",
    "display_name": "Quickstart"
  }'
```

```python
import httpx, os
H = {"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"}
scope = httpx.post(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaiqueue/scopes",
    headers=H,
    json={"slug": "quickstart", "display_name": "Quickstart"},
).json()["data"]
print(scope["id"])
```

```javascript
const res = await fetch(`${process.env.SCAIGRID_HOST}/v1/modules/scaiqueue/scopes`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ slug: "quickstart", display_name: "Quickstart" }),
});
const { data: scope } = await res.json();
console.log(scope.id);
```

Save the returned `scope.id` as `SCOPE_ID`.

## 2. Create a queue

Queues live inside scopes. Each queue has an ordering mode (`fifo`, `priority`, or `deadline`) and a consumer mode (`competing` or `broadcast`). For most work distribution you want `competing` + `fifo`.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE_ID/queues" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "tasks",
    "display_name": "Tasks",
    "consumer_mode": "competing",
    "ordering": "fifo",
    "max_depth": 0
  }'
```

```python
queue = httpx.post(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaiqueue/scopes/{scope['id']}/queues",
    headers=H,
    json={
        "slug": "tasks",
        "display_name": "Tasks",
        "consumer_mode": "competing",
        "ordering": "fifo",
    },
).json()["data"]
print(queue["id"])
```

```javascript
const qres = await fetch(
  `${process.env.SCAIGRID_HOST}/v1/modules/scaiqueue/scopes/${scope.id}/queues`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      slug: "tasks",
      display_name: "Tasks",
      consumer_mode: "competing",
      ordering: "fifo",
    }),
  },
);
const { data: queue } = await qres.json();
```

Save `queue.id` as `QUEUE_ID`.

## 3. Publish a message

Publishing writes a row in MariaDB and pushes the new message id into the queue's Redis pending sorted set. Use `idempotency_key` so retried publishes don't create duplicates. Labels and priority are optional but cheap to set early — routing rules consume both.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE_ID/queues/$QUEUE_ID/messages" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "task.run",
    "body": {"order_id": "ord_12345"},
    "priority": {"tier": "normal", "score": 500},
    "labels": {"region": "eu"},
    "idempotency_key": "ord_12345"
  }'
```

```python
msg = httpx.post(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaiqueue/scopes/{scope['id']}/queues/{queue['id']}/messages",
    headers=H,
    json={
        "type": "task.run",
        "body": {"order_id": "ord_12345"},
        "priority": {"tier": "normal", "score": 500},
        "labels": {"region": "eu"},
        "idempotency_key": "ord_12345",
    },
).json()["data"]
print(msg["id"], msg["state"])
```

```javascript
const pubres = await fetch(
  `${process.env.SCAIGRID_HOST}/v1/modules/scaiqueue/scopes/${scope.id}/queues/${queue.id}/messages`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type: "task.run",
      body: { order_id: "ord_12345" },
      priority: { tier: "normal", score: 500 },
      idempotency_key: "ord_12345",
    }),
  },
);
const { data: msg } = await pubres.json();
```

The same `idempotency_key` published twice returns the original message id instead of creating a duplicate.

## 4. Claim a message

Claim takes the next message off the queue and locks it for you. The lock lasts `visibility_timeout_s` — if you don't complete or fail the message within that window, the visibility-timeout enforcer puts it back as pending and another consumer can take it.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE_ID/queues/$QUEUE_ID/messages/claim" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batch_size": 1, "visibility_timeout_s": 60}'
```

The response is a list. Each claimed message includes the full body, the labels, and an `attempts` counter that ScaiQueue has already incremented. You have `visibility_timeout_s` seconds to call `complete`, `fail`, or `extend` — if you don't, the visibility-timeout enforcer returns it to `pending` and another consumer can claim it.

## 5. Complete the message

Completion moves the message to its terminal state and frees the slot. Pass any structured result you want downstream consumers to read in the `response` field — it ends up on the message row and in the event payload.

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiqueue/scopes/$SCOPE_ID/messages/$MSG_ID/complete" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"response": {"shipped": true}}'
```

The message moves to `completed`, the queue's `depth_claimed` decrements, and a `scaiqueue.message.completed` event is published on ScaiGrid's internal event bus carrying the message's `correlation_id` — so any worker (or ScaiCore checkpoint resolver) waiting on this id can react without polling.

## What just happened

- The scope you created is a row in MariaDB. Five system queues were created alongside it.
- Each publish wrote a row in `mod_scaiqueue_messages` and added the message id to a per-queue Redis sorted set scored by ordering mode (created_at for FIFO, deadline for deadline, tier+score for priority).
- The claim was a `ZPOPMIN` on that sorted set plus a `SET NX EX` claim lock — atomic across consumers.
- Completion released the Redis claim and decremented queue counters.

## Next

- [Architecture](./concepts/architecture) — the publish, claim, complete, reclaim loop in detail.
- [Topics, types, and HITL specs](./concepts/topics-and-types) — what makes a queue typed.
- [Build a pipeline with routing](./tutorials/build-a-pipeline) — multi-queue flow with routing rules.
- [Add a human review step](./tutorials/human-review) — HITL end-to-end.
