Skip to main content
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.
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.
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.
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.
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.
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:
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.
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.