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

# Logic & Actions

> Write typed backend logic with actions, queries, mutations, control flow, and assertions.

Actions are the functions of a SLang backend. They accept typed parameters, run backend logic, read and modify data, call modules, and return typed values.

## Action Shape

An action has a name, typed parameters, a return type, optional metadata, and either a `body` or an `external` implementation.

```slang theme={null}
entity User
  subject
  identity email
  fields
    email: EMAIL
    displayName: TEXT?

enum TaskStatus
  values
    todo
    in_progress
    done

entity Task
  fields
    title: TEXT
    status: TaskStatus := "todo"
    dueDate: DATE?
    completedAt: DATE_TIME?

relation User[tasks] 1 --- 0..* Task[owner]

action CreateTask(title: TEXT, dueDate?: DATE): Task
  description "Create a task for the current user."
  body
    user := @subject.entity
    task := create Task {
      owner := user
      title := title
      status := "todo"
      dueDate := dueDate
    }
    return task

action ListMyTasks(): Page<Task>
  description "Return a page of tasks owned by the current user."
  body
    user := @subject.entity
    return pageOf Task where owner == user

action CompleteTask(task: Task, completedAt: DATE_TIME): Task
  body
    update task {
      status := "done"
      completedAt := completedAt
    }
    return task

action DeleteTask(task: Task): VOID
  body
    delete task

action CloseAllOpenTasks(completedAt: DATE_TIME): NUMBER
  body
    page := pageOf Task where status != "done"
    count := 0
    for task in page.items
      update task {
        status := "done"
        completedAt := completedAt
      }
      count := count + 1
    return count
```

## Reading Data

Use `single` when the action needs one record. In HTTP contexts, a missing record is surfaced as a not-found response.

```slang theme={null}
entity Todo
  fields
    title: TEXT

action GetTodo(todoId: TEXT): Todo
  body
    todo := single Todo where @id == todoId
    return todo
```

Use `pageOf` when the action returns a collection. A page exposes `items`, `total`, and `offset`. Iterate over `page.items`, not over the page object itself.

`where` filters support a limited comparison shape. The left side must be a model field or `@id`. The right side must be an in-scope value, such as an action parameter or local variable, an internal value such as `@subject`, or a plain string literal.

```slang theme={null}
action ListTodoTasks(): Page<Task>
  body
    return pageOf Task where status == "todo"
```

Add `order by <field>` when callers need a stable, user-defined ordering. Ordering supports one persisted scalar field or `@id`; `asc` is the default direction, and `desc` reverses it.

```slang theme={null}
entity Profile
  fields
    name: TEXT

entity ProfileLink
  fields
    label: TEXT
    url: URL
    position: NUMBER

relation Profile[links] 1 --- 0..* ProfileLink[profile]

action ListProfileLinks(targetProfile: Profile): Page<ProfileLink>
  body
    return pageOf ProfileLink where profile == targetProfile order by position
```

## Building Nested Response Bodies

When an endpoint needs to return a parent object with related records nested inside it, query the parent record first, then query the child records and assign the child page's `items` array into the nested schema field.

```slang theme={null}
schema PublicLink
  fields
    title: TEXT
    url: URL

schema PublicPage
  fields
    slug: TEXT
    links: [PublicLink]

entity ProfilePage
  fields
    slug: TEXT

entity ProfileLink
  fields
    title: TEXT
    url: URL
    position: NUMBER

relation ProfilePage[links] 1 --- 0..* ProfileLink[page]

action CreateProfilePage(slug: TEXT): ProfilePage
  body
    page := create ProfilePage {
      slug := slug
    }
    return page

action CreateProfileLink(pageId: TEXT, title: TEXT, url: URL, position: NUMBER): ProfileLink
  body
    targetPage := single ProfilePage where @id == pageId
    link := create ProfileLink {
      page := targetPage
      title := title
      url := url
      position := position
    }
    return link

action GetPublicPage(slug: TEXT): PublicPage
  body
    profilePage := single ProfilePage where slug == slug
    links := pageOf ProfileLink where page == profilePage order by position
    return {
      slug := profilePage.slug
      links := links.items
    }

trigger CreateProfilePage on HttpRequest
  endpoint POST /profile-pages
  arguments
    slug := @request.body.slug

trigger CreateProfileLink on HttpRequest
  endpoint POST /profile-pages/{pageId}/links
  arguments
    pageId := @request.path.pageId
    title := @request.body.title
    url := @request.body.url
    position := @request.body.position

trigger GetPublicPage on HttpRequest
  endpoint GET /public/pages/{slug}
  body {
    slug := @result.slug
    links := @result.links
  }
  arguments
    slug := @request.path.slug
```

After creating a page and two links, `GET /public/pages/alice` returns a nested `links` array. Because `links := links.items` returns the queried `ProfileLink` records directly, each nested link includes the record fields returned by the API, such as `id`, `title`, `url`, `position`, `page`, `createdAt`, and `updatedAt`.

## Writing Data

Use `create Entity { ... }` to insert a record. Use `update record { ... }` to patch fields on an existing record. Use `delete record` to remove one.

When you update a variable holding an entity instance, the variable is rebound to the updated instance, so returning the same variable returns the latest fields.

## Strings

Use double-quoted strings for text. For dynamic text, prefer template string interpolation with `${...}` inside the string:

```slang theme={null}
action BuildTaskPrompt(task: Task, categoryNames: TEXT): TEXT
  body
    return "Classify this task into one of these categories: ${categoryNames}
Task title: ${task.title}
Task description: ${task.description}"
```

Interpolations can include values that resolve to `TEXT`, `NUMBER`, or `BOOLEAN`. For entities, files, pages, and other structured values, interpolate a specific scalar field such as `${task.title}` rather than the whole value.

The `+` operator can concatenate two text-compatible operands, such as `TEXT`, `EMAIL`, `PHONE_NUMBER`, `ADDRESS`, or `URL`, and can include `NUMBER` or `BOOLEAN` on the other side. Keep each `+` expression binary. Do not write long chains such as `"Task: " + task.title + ", categories: " + categoryNames`; use a template string, split the expression into steps, or parenthesize each binary pair.

```slang theme={null}
action BuildCategoryList(user: User): TEXT
  body
    categories := pageOf Category where owner == user
    categoryNames := ""
    for category in categories.items
      categoryNames := "${categoryNames}${category.name}, "
    return categoryNames
```

## Control Flow

SLang supports:

* `if` / `else`
* `for item in array`
* `while`
* `break` and `continue`
* boolean logic with `and`, `or`, and `not`
* arithmetic and comparisons for compatible primitive values

Use `assert(description := "...", rule := condition)` when an action should fail unless an invariant is true.
