# Walkthrough: Describe the Work, Get a Governed Schema

> End to end with an AI agent — sign in, describe a threaded discussion board in business terms, and get tables, columns, and physical names that follow your team standard.

<iframe src="https://www.youtube-nocookie.com/embed/MqHWtiHfBI0" title="Building a Database Schema With an AI Agent — Without Naming a Single Column" style="width:100%;aspect-ratio:16/9;border:0;border-radius:8px;margin:0.5rem 0 1rem;" allow="accelerometer; clipboard-write; encrypted-media; picture-in-picture; web-share" allowfullscreen></iframe>

Ask any chatbot for a discussion-board schema and you get one. The problem
is never getting *a* schema — it is getting the schema **your team already
agreed on**: the same abbreviations, the same suffixes, the same word for
the same idea, in the tenth table as in the first.

This walkthrough goes from an empty project to standards-compliant DDL
without leaving your agent. The example is a threaded discussion board —
small enough to follow, and it forces the one case that trips most tools:
a post that replies to another post.

Every output below is copied from a real run.

## Before you start

- Node.js 22 or later, and `sqemo-mcp` **2.2.0 or later**
- The MCP server registered with your agent, and `npx sqemo-mcp login`
  done — see [AI Integration (MCP)](/docs/mcp/)
- A workspace with a [team standard](/docs/naming-standards/). This
  walkthrough uses one whose word list already contains *Customer*,
  *Order*, *Number*, *Content*, *Flag*, and a dozen more

## 1. Start the project on the standard

A new ERD has an empty word list, so physical names come out as the
logical names verbatim. Bind the project to your standard **at creation**
and the word list, naming rules, and domains come with it:

```jsonc
list_workspaces
// → [{ workspaceId: "…", name: "Engineering", role: "owner",
//      glossary: { version: 4, … } }]

create_erd {
  name: "Threaded Discussion Board",
  workspaceId: "…",
  target: { server: true }
}
```

```jsonc
get_erd_overview
// → { glossaryLinked: true, dictionaryEntryCount: 28, domainCount: 15, … }
```

That `glossaryLinked: true` is the whole point. From here on, every name
the agent creates is resolved through the standard rather than invented.

## 2. Describe the work in business terms

Now talk to your agent the way you would talk to a colleague:

> Model a discussion board where members post articles, a post can be a
> reply to another post, and members comment on posts.

The agent calls `upsert_entity` and `upsert_attribute` with **logical**
names. You never type a column name:

```jsonc
upsert_entity   { logicalName: "Post" }
// → { physicalName: "POST" }

upsert_attribute { logicalName: "Post Content", domain: "Content" }
// → { physicalName: "POST_CNTS" }

upsert_attribute { logicalName: "Delete Flag", domain: "Flag" }
// → { physicalName: "DELETE_YN" }
```

`Content` → `CNTS` and `Flag` → `YN` are not the agent's taste. They are
your word list's abbreviations, applied the same way they were applied in
every other table your team has modelled.

Domains carry the data type, so `Content` is `varchar(1000)` everywhere it
appears — nobody decides column widths per table.

## 3. The reply-to-a-post case

A self-referencing foreign key cannot reuse the primary key's name, so it
gets a **role prefix**. The prefix is a naming rule, and — like every
other word — it is resolved through the word list:

```jsonc
upsert_relationship {
  sourceEntityId: "<Post>", targetEntityId: "<Post>",
  cardinality: "1:N", relationshipType: "nonIdentifying",
  constraintName: "FK_POST_PARENT"
}
```

```sql
`PARENT_POST_NO` varchar(20) COMMENT 'Parent Post Number'
```

The prefix defaults to `Parent`, so the FK above comes out as
`PARENT_POST_NO` with no configuration. Change it once on your standard —
in the app, **Naming tab → Edit → Self-reference prefix** — and every
self-referencing FK in every connected project follows it.

Two things worth knowing before you change it:

- Register the prefix as a word first if you want it abbreviated
  (`Parent` → `PRNT` gives `PRNT_POST_NO` instead of `PARENT_POST_NO`).
- Unlike ordinary column names, **existing self-referencing FKs are
  renamed** on their next edit, because their names are derived rather
  than stored. Everything else keeps the name it already has.

Projects created before this default changed don't shift underneath you.
When one loads, an unset prefix is written into its naming rules: `상위`
(Korean for "parent") if it already has a self-referencing relationship, so those
column names stay exactly as they were — `Parent` if it doesn't.

## 4. Words the standard doesn't have yet

Real modelling hits words nobody registered. Sqemo does not quietly invent
an abbreviation — it flags them:

```jsonc
lint_erd
// → { code: "unknown-word", severity: "warning",
//      objectName: "Delete Flag",
//      message: "'Delete Flag' contains words not registered in the word list." }
```

`Delete` is missing, so `DELETE_YN` came out unabbreviated. The fix is not
to hardcode a name — it is to propose the word:

```jsonc
propose_dictionary_word {
  logicalWord: "Delete", physicalWord: "DELETE", abbreviation: "DEL",
  note: "Found while modelling the discussion board"
}
// → { proposalId: "…", status: "pending", baseVersion: 4 }
```

The standard's owner approves it in the app, and the abbreviation
propagates to every connected project. This is the part a chatbot cannot
do: the agent is a **proposer**, not the authority.

## 5. Check compliance

`check_naming` compares a physical name you already have against the one
the standard would generate:

```jsonc
check_naming {
  logicalName: "Customer Phone Number",
  physicalName: "CUSTOMER_PHONE_NUMBER"
}
// → { generatedPhysicalName: "CUST_TEL_NO",
//      compliant: false, providedMatches: false }
```

That is the check to put in CI. It works on a project file with no agent
involved:

```bash
npx sqemo-mcp lint schema.erd.json   # exit code 1 on violations
```

## 6. Export

```jsonc
export_sql { dialect: "mysql" }
```

```sql
CREATE TABLE `POST` (
  `POST_NO`        varchar(20)   NOT NULL COMMENT 'Post Number',
  `POST_CNTS`      varchar(1000) NOT NULL COMMENT 'Post Content',
  `DELETE_YN`      char(1)       NOT NULL DEFAULT 'N' COMMENT 'Delete Flag',
  `PARENT_POST_NO` varchar(20)            COMMENT 'Parent Post Number',
  PRIMARY KEY (`POST_NO`)
) COMMENT='An article on a board; may reply to another post';

ALTER TABLE `POST` ADD CONSTRAINT `FK_POST_PARENT`
  FOREIGN KEY (`PARENT_POST_NO`) REFERENCES `POST` (`POST_NO`);
```

Logical names survive as column comments, so the business meaning reaches
the database instead of dying in the diagram.

## One gotcha

`defaultValue` is raw SQL, emitted verbatim after `DEFAULT`. Quote string
literals yourself:

```jsonc
upsert_attribute { logicalName: "Delete Flag", defaultValue: "'N'" }  // ✅
upsert_attribute { logicalName: "Delete Flag", defaultValue: "N" }    // ❌ DEFAULT N
```

Unquoted values are treated as expressions, which is what you want for
`0` or `CURRENT_TIMESTAMP` — and what you do not want for `N`.

## What actually happened here

The agent did the typing. It did not decide anything that matters:

| Decision | Made by |
|---|---|
| Which tables and relationships exist | You, in business terms |
| Abbreviations, separators, case | Your team standard |
| Data types | Your domains |
| New words entering the standard | The standard's owner, by approval |

That is the difference between generating a schema and growing one your
team can keep. Next: [Naming Standards](/docs/naming-standards/) for how
the word list and rules work, or [Team collaboration](/docs/collaboration/)
for the proposal queue and roles — the queue is also a
[two-minute video](https://youtu.be/wHsoFbak6GY), if you would rather watch
a word get approved than read about it.

---
Source: https://sqemo.com/docs/ai-walkthrough/ · Updated 2026-08-13
