Basic Code reads and writes tables managed by people in the screen from skills. The prompt includes a list {{$테이블.필드}} that shows data you see.
Why is this entry point necessary? Basic Code stores JSONB in the data column of the code_rows table. Without this entry point, a skill would have to know that storage structure and write SQL with Zio.raw()—a raw access to the engine’s internal structure—and if the storage method changes, all skills for every instance will break together.
The engine does not know what tables exist. Construction uses 공종, fashion uses 브랜드, food service uses 점포. Both table names and column names are passed only as arguments. Do not put code that assumes a particular table into the SDK or engine.
Method
Description
Zio.code(table_name)
Specifies one Basic Code table. Use the Table ID (English name) from the Basic Code screen. If you specify a non‑existent table, a LookupError is raised—typos are not silently turned into an empty list.
.where(**kwargs)
Filters rows. Uses comparison suffixes as they are. (e.g., use_yn="Y", client_name__startswith="GS")
.where_or(**kwargs)
Groups multiple conditions on the same table with OR (different from other .where() which is AND). Use when any one of several columns matches—previously you had to split queries and combine them in Python. Suffixes (__contains, etc.) are the same as .where(). Calling it multiple times creates separate OR groups that are combined with AND. It only applies to read operations (fetch()·count())—applying OR to update()·delete() can affect more rows than intended, so it is disallowed. (e.g., .where_or(client_name__contains=kw, aliases__contains=kw))
Zio.code.list()
First checks what tables exist. Returns [{table_name, title, description, field_count}]—no rows are returned. Do not repeatedly guess with Zio.code("guess"); choose from here.
.fetch()
Retrieves a list of rows as GraphResult. Each row is a flat dict whose keys are the column names unchanged. The row identifier _id is included.
.info()
Gets a dict describing the table itself: {table_name, title, description, fields:[{name, label, type}]}. For tables without a title, title defaults to the table name.
.create(props)
Inserts one row. Duplicates are not checked—what is considered duplicate varies by table, so check with .where(...).fetch() before inserting.
.update(props)
Updates rows that match the condition. Only the specified columns are overwritten; others remain unchanged. If no condition is given, a ValueError is raised.
.delete()
Deletes rows that match the condition. Returns the deleted rows—so you know what was removed. If no condition is given, a ValueError is raised.
.create_table(columns, title, description)
Creates a table. columns=[{name, label, type}]. The created table is managed by people in the screen afterward—skills do not lock it. When it is first created there are no locks (the same as a table made in the screen).
.add_column(column)
Adds one column at the end. {name, label, type}. Existing rows will show that column empty—values are not inserted.
.drop_column(column_name)
Removes a column from the declaration. Values already stored in rows are not deleted—if you add a column with the same name again they reappear. The values disappear from the screen but are still returned by .fetch() (unless read is filtered out). Trying to use a dropped column later raises ValueError. You cannot drop the last remaining column.
.rename_column(old_name, new_name)
Renames a column and moves its values along with it. If you only change the declaration, the old key remains in the data, is not deleted, and becomes invisible.
.drop_table(force=False)
Deletes the table and all its rows. If other parts of the system reference this table, it refuses and tells you what references it. To delete anyway, set force=True. Returns {table_name, deleted_rows, dependencies}.
.resolve(value, name, alias=None)
Checks if this table knows a notation. If yes, returns the official name; otherwise returns None. It only matches exactly 100%—case, whitespace, and punctuation are ignored (GS 25 = GS25).
.suggest(value, name, alias=None, limit=3)
Picks similar official names. When .resolve() fails, this provides candidate options for the user. The input includes aliases; the output is only official names.
.name_conflicts(name, alias=None)
Reports aliases that belong to two different official names: [{"alias": "편의점", "claimed_by": ["GS25", "CVSNET"]}]. An empty list means no conflicts.
Locks are written in the table itself. The SDK sees the same values that the screen (Basic Code management) sees—if there are two judges, you can have a gap where “the screen cannot do it but the skill can.”
Field
What it locks
is_data_user_editable
The entire row. If off, only read (fetch·count·info) is allowed
allowed_actions
Which actions are permitted. If ["update"], values can be changed but rows cannot be added or deleted. Empty means all allowed.
When blocked, a sentence explaining why it is blocked and what is allowed appears. Read that sentence and stop—creating a new table with the same name or repeating the same call does not solve it. Locks are removed by people in the screen.
PermissionError: 'llm_callers' 표는 삭제를 허용하지 않습니다. 이 표에 허용된 것: 수정.
PermissionError: 'llms' 표는 칸 구성이 잠겨 있습니다(칸 추가 거부).
시스템이 칸 이름으로 읽는 표라, 바뀌면 조회가 조용히 어긋납니다.
행은 잠금과 별개입니다 — 값만 고치려면 create·update·delete 를 쓰십시오.
Locked tables are system tables read by the engine using column names (llms·llm_callers·system_configs·collectors·common_code). Tables created by a manager are not locked—whether made in the screen or set up by a skill, they behave the same.
Which column is the official name and which is an alias is known only to the person who created the table. Construction uses 공종명, fashion uses 브랜드명, and some tables do not have an alias column at all. If the SDK pre‑sets a name like aliases, it becomes industry‑specific, and for tables without an alias column it silently does nothing.
There is a reason why .resolve() and .suggest() are not combined into one function. Mixing “known” and “similar” would cause unnecessary or missing prompts. If .resolve() gives an answer, you pass; if not, you get suggestions from .suggest() to ask the user—this two‑step flow is standard.
Do not touch a dictionary that is wrong. When an alias belongs to two official names (e.g., 편의점 for two convenience store companies), which one it attaches to is determined by the order in the table, and no one can later find out why it was chosen. .resolve()·.suggest() never use such aliases, so they are not attached incorrectly, but if you want to know that a dictionary is wrong, call .name_conflicts(). It does not tell you what to do—whether to skip the entire table is a domain decision.
Reading a table and reading about a table are different. .fetch() gives you a list of clients, while .info() tells you that the table is “major clients.”
# 나쁨 — 사람은 'client' 가 뭔지 모른다
질문 =f"기초코드에 없는 값입니다: '{값}'"
# 나쁨 — 담당자가 화면에서 표 이름을 바꿔도 문장은 안 따라온다
질문 =f"주요거래처에 없는 값입니다: '{값}'"
# 좋음
title = Zio.code(code_table).info()["title"]
질문 =f"{title}에 없는 값입니다: '{값}'. 어느 쪽입니까?"
The Korean name of a table is the one the domain manager attached in the screen. If you write it into a skill, from then on both places will have the same name, and the manager may not understand why their changed name does not appear on the screen.
Reject update·delete without conditions. If reference data disappears all at once, there is no way to recover it. Always use .where().
Reject columns that are not declared. The screen only displays declared columns, so if you accept a value for an undeclared column it will be stored but never visible and later nobody can find it. The error message includes the columns usable in that table.
The owner of Basic Code is the domain manager. A skill adds one line to a table set up by people in the screen; it does not write into its own storage. If you need skills to create or delete tables, use Zio.entity()—that’s for data whose state changes and disappears (job queues, processing history).
Practical Example of Basic Code Usage — Normalizing Notation
Even if a model outputs GS THE FRESH, you want it stored in the graph as GS더프레쉬. Where to collect which notation is only written in Basic Code; the engine does not know those rules.
Note that dry_run defaults to True. .consolidate_into() cannot be undone, so it’s safest for a skill to first show what will be merged and then execute in two steps.
The options stored in unknown become buttons on the prompt screen. It is fine if the candidate list is empty—then a person can write it directly, put that answer into the alias column, and from next time .resolve() will find it immediately. The prompt ends after one round of questioning.