Actions

In Keel, models not only describe what your data looks like, but also how it behaves. This is done using Actions, which is a general term for two features in Keel: Built-In Actions and Functions. An built-in action is implemented by Keel purely based on what is written in your schema and requires no extra code to work. Functions are also defined in your schema but are implemented in code by you.

As a simple example, the following schema has one action which lets you fetch a product by its id. As it is defined as an operation this schema requires no code to be written.

model Product {
  actions {
    get getProduct(id)
  }
}

To find out how to use your Actions from your API see the API docs.

🙌

In the future Actions will also be used to auto-generate internal tools for you and your team in the Keel console.

Action Types

Every Action has a type which indicates what it does. The action types get, list, create, update, and delete can be used for built-in actions and functions. There are also two more generic action types - read and write which can only be used for functions and are discussed in more detail in the Functions documentation. The table below describes the behaviour of each action type.

TypeBehaviourOperationFunction
getReturns a single record or null
listReturns a list of records, supports complex filtering, pagination, and sorting
createCreates a new record and returns it
updateUpdates a record and returns it
deleteDeletes a record and returns the id of the deleted record
readRead and return data of any type
writePerform updates to your data and return data of any type

A model can have multiple actions of the same type.

Action Names

Action names must be written in lowerCamelCase and must be unique across your whole app, not just within each model.

get

A get Action is for reading a single record, for example reading a record by it's built-in id field.

model Book {
  actions {
    get getBook(id)
  }
}

Using a unique field

get built-in actions support reading a record by any unique field.

model Book {
  fields {
    isbn Text @unique
  }
  actions {
    get getBook(isbn)
  }
}

Multiple optional inputs

If you want to be able to read a record using either its id or another one of its unique fields, you can do so by providing multiple inputs and marking them all as optional using ?.

model Book {
  fields {
    isbn Text @unique
  }
  actions {
    get getBook(id?, isbn?)
  }
}

Using a relationship field

It is possible to look up a record by a unique field of a related record. For example, you might want to get the author of a book using its ISBN. This is possible if:

  • A book has only one author
  • The isbn field is marked as @unique

Inputs that reference fields on related models use dot-notation as shown in the example below.

model Author {
  fields {
    books Book[]
  }
  actions {
    get getAuthorByIsbn(books.isbn)
  }
}
 
model Book {
  fields {
    author Author
    isbn Text @unique
  }
}

Using @where

You can use the @where attribute in a get operation to filter records. One use case for this is defining an action that returns data based on the calling identity. We dive deeper into identities in the Identity docs but for now you can think of them as a way to identify the caller of an API. In the example below we define an action that returns the profile of the caller.

model Profile {
  fields {
    identity Identity @unique
  }
  actions {
    get myProfile() {
      @where(profile.identity == ctx.identity)
    }
  }
}

In the above schema, myProfile is valid even though it doesn't accept any inputs. This is because the expression in the @where attribute filters on the identity field, which is marked as being unique.

Validation

For get built-in actions, Keel will validate that you are filtering on at least one unique field. There are no restrictions on the inputs accepted by functions, but they must return a single record. Refer to the Functions documentation for further information.

create

A create Action is for creating a new record.

model Book {
  fields {
    title Text
    genre Text
  }
 
  actions {
    create createBook() with (title, genre)
  }
}

As shown in the above schema, the inputs for a create action come after the with keyword. This is because the first set of inputs (those that come after the action name) are read inputs, which are used for reading or filtering data. The inputs after the with keyword are write inputs, which are used for writing data. Since a create action creates a new record, it will never accept any read inputs.

Using @set

You can use the @set attribute in a create operation to set fields on the newly created record. One use case for this is to set the calling identity on a field that uses the Identity model.

model Customer {
  fields {
    name Text
    identity Identity
  }
  actions {
    create newCustomer() with (name) {
      @set(customer.identity = ctx.identity)
    }
  }
}

In this example we accept the name field as in input to the action but set the identity field based on the callers Identity. For more information on Identity see the Identity docs.

Creating nested records

It is possible to create nested records using a create operation by using dot-notation to define the input fields.

model Lesson {
  fields {
    title Text
    date Date
    course Course
  }
}
 
model Course {
  fields {
    name Text
    lessons Lesson[]
  }
  actions {
    create createCourse() with (name, lessons.title, lessons.date)
  }
}
 

The createCourse action allows you create a course along with a number of linked lessons. The course field of the newly created Lesson records will be automatically set to the newly created Course.

Linking to existing records

When creating a record you can link existing records by specifying the id of the related field. In the example below we can create a new Lesson but set its course field to an existing Course.

model Course {}
 
model Lesson {
  fields {
    title Text
    date Date
    course Course
  }
  actions {
    create createLesson() with (title, date, course.id)
  }
}

Validation

For create built-in actions, Keel will validate that you are providing all required fields. Fields can be set using inputs, a @default attribute on the field, or a @set attribute. For functions, there are no restrictions on what inputs you accept, just that your functions creates and returns a single record. For more information see the Function docs.

update

An update Action is for updating an existing record. As you need to specify both which record you want to update and the fields to be updated, an update Action can have both read and write inputs.

model Book {
  fields {
    title Text
  }
  actions {
    update updateBook(id) with (title)
  }
}
 

The updateBook operation in the above schema lets you update the title field of a book identified by it's id.

Using a unique field for lookup

As with get, you can use any unique field to identify the record that should be updated. The following schema shows how you can update a book by its unique isbn field.

model Book {
  fields {
    isbn Text @unique
    title Text
  }
  actions {
    update updateBook(isbn) with (title)
  }
}

Using optional write inputs

By using optional inputs, it is possible to define update actions that allow the caller to choose which fields they want to update. In the following schema, we define an action called updateBook which permits the title and publishDate fields to be updated. Both fields are marked as optional, meaning the caller can provide either one, both, or neither.

model Book {
  fields {
    title Text
    publishDate Date
  }
  actions {
    update updateBook(id) with (title?, publishDate?)
  }
}

Using @set

You can use the @set attribute in an update Operation to update additional fields on top of those provided as write inputs. In fact if you use @set you don't need to have any write inputs at all. To illustrate this we can make a publishBook Action that just accepts the id of the book to publish and then uses the @set attribute to set the publishDate field to the current date using ctx.now.

model Book {
  fields {
    publishDate Date
  }
  actions {
    update publishBook(id) {
      @set(book.publishDate = ctx.now)
    }
  }
}

Expressions used in a @set attribute can also reference enums from your schema. Therefore, another use case for @set would be to create specific actions for changing the status of a record. In the schema below we have specific actions for starting and completing a Todo.

enum Status {
  Todo
  InProgress
  Completed
}
 
model Todo {
  fields {
    status Status
    startedAt Timestamp?
    completedAt Timestamp?
  }
  actions {
   update startTodo(id) {
      @set(todo.status = Status.InProgress)
      @set(todo.startedAt = ctx.now)
    }
    update completeTodo(id) {
      @set(todo.status = Status.Completed)
      @set(todo.completedAt = ctx.now)
    }
  }
}

Validation

For update built-in actions, Keel will validate that you are filtering on at least one unique field. For Functions there are no restrictions on what inputs you accept, just that your function updates and returns a single record. For more information see the Function docs.

list

A list Action is for reading a number of records and supports complex filtering on inputs, pagination and sorting.

model Book {
  fields {
    title Text
  }
  actions {
    list listBooks(title)
  }
}

The listBooks Operation in the above schema would allow for any of the following use-cases:

  • Find all books with the exact title "Great Gatsby"
  • Find all books whose title contains the text "at Ga"
  • Find all books whose title starts with the text "Gre"
  • Find all books whose title ends with the text "atsby"
  • Find all books whose title is either "Great Gatsby" or "Great Expectations"

For more info on how filtering works in list actions see the API docs.

Filtering on multiple fields

A list Action can accept multiple inputs which are combined with AND.

model Book {
  fields {
    title Text
    publishDate Date
  }
  actions {
    list listBooks(title, publishDate)
  }
}

With this schema it would be possible to use the listBooks operation to do things like:

  • Find all books whose title contains the text "Adventure" and which were published in 1954
  • Find all books whose title starts with the text "Great" and which were published between 1990 and 1995

Using @where

You can use the @where attribute in list built-in actions to provide some additional filters that are applied on top of the inputs. As an example we could make a list operation that only returned completed Todo's.

model Todo {
  fields {
    completed Boolean
  }
  actions {
    list completedTodos() {
      @where(todo.completed == false)
    }
  }
}

Using @orderBy

You can use the @orderBy attribute in list actions to describe how the results are ordered. By default, items are sorted by createdAt.

model Contestant {
  fields {
    name Text
    gold Number
    silver Number
    bronze Number
  }
 
  actions {
    list listRankings(name?) {
      @orderBy(gold: desc, silver: desc, bronze: desc)
    }
  }
}

Using @sortable

When you want the client to have control on the sorting order. You can use the @sortable attribute in list built-in actions to select which fields are available to sort by. The sort order can then be controlled by passing orderBy in the request.

model Post {
  fields {
    name Text
    likes Number
  }
 
  actions {
    list listPosts() {
      @sortable(createdAt, likes)
    }
  }
}
💡

When using both @orderBy and @sortable, the client provided sort values will override the @orderBy ordering.

delete

A delete Action is for deleting a single record. It's pretty much analogous to get, just rather than reading and returning the record it deletes the record and returns the value of its id field.

model Book {
  actions {
    delete deleteBook(id)
  }
}

The @embed attribute

The @embed attribute in Keel allows you to include related fields as part of the response for list and get actions automatically. This is useful for embedding related data directly within your model responses without the need for additional requests using the JSON API. The @embed attributes accepts one or more arguments. These arguments must be fields assigned to the model for which we're defining the action and must be related models.

model User {
  fields {
    name Text
    email Text
  }
}
 
model Review {
  fields {
    content Text
    reviewer User
    post Post
  }
}
 
model Post {
  fields {
    title Text
    content Text
    author User
    reviews Review[]
  }
 
  actions {
    get getPost(id) {
      @embed(author)
    }
    list listPosts() {
      @embed(author)
    }
  }
}

In this example, the Post model's getPost action includes the author field, which is a User model. The @embed(author) attribute indicates that when retrieving a post, we want to include the author as part of the response.

Nesting embedded fields

You can nest embedded fields to include deeper relationships using the dot notation. Given the example above, we could include @embed(author, reviews.reviewer) as part of our getPost action. The response will now include all the required data, e.g.:

{
  "author": {
    "createdAt": "string",
    "email": "string",
    "id": "string",
    "name": "string",
    "updatedAt": "string"
  },
  "content": "string",
  "createdAt": "string",
  "id": "string",
  "reviews": [
    {
      "content": "string",
      "createdAt": "string",
      "id": "string",
      "postId": "string",
      "reviewer": {
        "createdAt": "string",
        "email": "string",
        "id": "string",
        "name": "string",
        "updatedAt": "string"
      },
      "updatedAt": "string"
    }
  ],
  "title": "string",
  "updatedAt": "string"
}
💬

Performance considerations: Embedding fields can impact performance, especially with deep or multiple embeddings. Use this feature judiciously.