---
summary: "Turn a running Core into a chat model in ScaiGrid's catalogue \u2014 slug,\
  \ group exposure, sync, unpublish."
title: Publish as model
path: modules/scaicore/tutorials/publish-as-model
status: published
---

A published Core appears in ScaiGrid's model catalogue and is callable through `POST /v1/inference/chat` exactly like any other backend. This is how you let arbitrary ScaiGrid clients chat with the agent without knowing they're talking to a ScaiCore program.

This page is about the ScaiGrid wrapper's publish surface. For how the program itself answers — its DSL, its flows, its tool calls — see [/docs/scaicore](https://www.scailabs.ai/docs/scaicore).

## What publish does

When you call `POST /cores/{id}/publish`, the wrapper:

1. Creates a `BackendModel` row with `provider_type=scaicore` and `connection_config.core_id` set.
2. Creates a `FrontendModel` row with slug `scaicore/{tenant_slug}/{core_slug}`, the Core's name, description (pulled from `metadata.description` or `source.identity.persona`), the Core's avatar, and `capabilities = {tool_use: true, streaming: false}`.
3. Wires the front-end model to the back-end with a `ModelBackend` link.
4. Writes `published`, `published_model_id`, `published_backend_id`, `published_slug` into the Core's metadata so unpublish can find it again.
5. Optionally adds the new model slug to any model groups you list — with scope validation: non-super-admins cannot add to `global` groups or to partner / tenant groups they don't belong to.

The Core stays `running` (or whatever state it was in). Publication is metadata-only — the Core itself doesn't restart.

## Publish

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/cores/$CORE_ID/publish" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "group_ids": ["mg_internal_chat", "mg_qa"] }'
```

```python
core = httpx.post(
    f"{os.environ['SCAIGRID_HOST']}/v1/modules/scaicore/cores/{core_id}/publish",
    headers={"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"},
    json={"group_ids": ["mg_internal_chat", "mg_qa"]},
).json()["data"]
print(core["metadata"]["published_slug"])
```

```javascript
const res = await fetch(`${process.env.SCAIGRID_HOST}/v1/modules/scaicore/cores/${coreId}/publish`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ group_ids: ["mg_internal_chat", "mg_qa"] }),
});
const { data: core } = await res.json();
console.log(core.metadata.published_slug);
```

`group_ids` is optional. If omitted (or if the caller lacks `models:manage`), the new model exists in the catalogue but is not added to any group — clients with group-restricted access will not see it.

If the Core is already published, the call returns a 400 `VALIDATION_ERROR` ("Core is already published as a model"). Use **sync** to update an existing publication.

## Call the published model

The slug is your standard ScaiGrid model id:

```bash
curl -X POST "$SCAIGRID_HOST/v1/inference/chat" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "scaicore/acme/approval-agent",
    "messages": [{"role": "user", "content": "Please review this expense."}]
  }'
```

Routing, accounting, audit, and rate-limiting all work the same as for any other model. The dispatcher resolves the `scaicore` provider and forwards the request into the Core through its HostEnvironment.

## Sync after edits

If you change the Core's name, description, persona, or avatar after publishing, the FrontendModel row goes stale. The publishing service exposes a `sync()` method that re-reads the Core and rewrites the FrontendModel's `display_name`, `description`, `system_prompt_template`, `avatar_url`, and `metadata_`. Today this is called internally on edit paths; there is no separate HTTP endpoint for it.

## Unpublish

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaicore/cores/$CORE_ID/unpublish" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"
```

Unpublish removes the slug from every model group it was added to, deletes the FrontendModel (which cascades the ModelBackend link), deletes the BackendModel, and clears the `published_*` keys from the Core's metadata. The Core itself is untouched.

If the Core was never published, you get a 400 `VALIDATION_ERROR` ("Core is not published").

## Scope rules for `group_ids`

The wrapper validates each requested group against the caller's scope:

| Caller | Allowed groups |
|---|---|
| Super admin | Any (global, partner, tenant). |
| Partner admin | `partner` scoped to *your* partner; `tenant` scoped to your tenants. |
| Tenant admin | `tenant` scoped to *your* tenant. |

Groups outside your scope are silently dropped from the request — the publish still succeeds, but the model is not added to those groups. There is no error for "you tried to add to a group you can't see." If you need the explicit feedback, list groups via the standard `/v1/models/groups` endpoints first.

## What you can't do (yet)

- **Stream from a published Core.** Capabilities are set with `streaming: false`. The Core's response is materialised then returned; long-running flows that yield tokens incrementally are not exposed through `/v1/inference/chat` streaming today.
- **Direct OpenAI-compat dispatch.** Published Cores work through the native `/v1/inference/chat` endpoint. The `/oai/v1/` compat layer routes through the same dispatcher, so it will work, but the wrapper is built around the native API as primary (see ScaiGrid's API hierarchy notes).

## Common gotchas

- **No groups, no visibility.** Publishing without `group_ids` leaves the model out of every group. Clients with group-restricted scopes won't see it. If you want public visibility for the tenant, add the slug to your tenant's default model group.
- **Slug collisions.** Slug is `scaicore/{tenant_slug}/{core_slug}` — already-unique-by-design, no collision possible unless you somehow forge a tenant slug. If you do hit a collision, the underlying DB write will fail with a uniqueness error.
- **Re-publish requires unpublish.** Calling `publish` on an already-published Core errors out. Unpublish first, then publish again. Edits to the published row use `sync()`, not republish.
