Building a Contact Form with Records

Last edit: Jun 19, 2024

This guide will help you understand the process of building a contact form backed up with Record. Whenever you need to build a simple contact form, blog functionality, or complex API endpoint, you can define any data structure you need with Records.



Building a Contact Form with Records is a nine-step process:

Step 1: Define Contact – Table

To define a new Table, create a YAML file in the schema directory. The name of your table will default to the name of your file. Therefore, create a file named app/schema/contact.yml. To build a Contact Form, you'll need an email of a person who submits the request (to be able to reply to them) along with their name, and the description of a problem. All of these Properties should be strings. The table definition should look as follows:

name: contact
- name: name
  type: string
- name: email
  type: string
- name: description
  type: string

For more information on Properties please visit the Properties Documentation.

Step 2: Define GraphQL Mutation to Persist Records in Contact Table

With the Contact table defined, the next step is to create a GraphQL mutation to persist records in the database. To do it, use record_create GraphQL mutation. Create a file namedapp/graphql/contacts/create.graphql with the following content:

mutation contact_create($name: String!, $email: String!, $description: String!) { # we define arguments as mandatory by adding "!" after the type
    record: {
      table: "contact"
      properties: [
        # we use "value" because the property type is a string, if it was for example an int you would use value_int etc.
        { name: "name", value: $name }
        { name: "email", value: $email }
        { name: "description", value: $description }
  ) {
    # we use "property" because the property type is a string, if it was for example an int, you would use property_int
    name: property(name: "name")
    email: property(name: "email")
    description: property(name: "description")
    # for each new record we automatically store created_at so you don't have to remember about it

You can develop and run GraphQL queries and mutations using pos-cli gui serve:

Screenshot of Create Contact GraphQL

Upon successful invocation, you will be able to see the newly created record in the Database Management UI:

Screenshot of records inside Contact table

Step 3: Fetch Saved Data with GraphQL

Now that you have created the Contact Table, the next step is to fetch the data using GraphQL. To define the contacts/find query, create a file named app/graphql/contacts/find.graphql:

query contacts_find($id: ID) {
    filter: {
      table: { value: "contact" },
      id: { value: $id }
    per_page: 100
  ) {
    results {
      name: property(name: "name")
      email: property(name: "email")
      description: property(name: "description")

To check if the above query works properly you can head to the next section to learn how to embed query results with page view or use pos-cli gui

Step 4: Render Records Within a Page

Display contacts as a list with the possibility to get to the contact details view.
As a first step, you need two pages that will respond to given paths:

  • GET /contacts for list view
  • GET /contacts/:id for detail view

List View
Define the first endpoint in app/views/pages/contacts/index.liquid:


{% liquid
  graphql g = "contacts/find"
  assign contacts = g.records.results

<h1>List View</h1>
  {% for contact in contacts %}
    <td>{{ contact.id }}</td>
    <td>{{ contact.email }}</td>
    <td><a href="/contacts/{{ contact.id }}">Details</a></td>
  {% endfor %}

This code runs a GraphQL query to fetch all contacts and displays them in a table. Each row includes a link to the detailed view of the contact.

Detail View
Define the detail view in app/views/pages/contacts/show.liquid:

slug: contacts/:id

{% liquid
  graphql g = "contacts/find", id: context.params.id
  assign contact = g.records.results.first

<h1>Detail view</h1>
<p>{{ contact.name }}</p>
<p>{{ contact.email }}</p>
<p>{{ contact.description }}</p>

This code runs a GraphQL query to fetch the contact details based on the id from the URL. The details are then displayed in a simple layout.

For more information, visit the Pages documentation. In a real-world application, you should ensure that only administrators have access to contact requests, and you should implement authentication and authorization. Additionally, you usually want to separate business logic from the presentation layer. We recommend not including HTML directly in the page and using Liquid Partials for presentation purposes. We recommend to treat the Page as a controller, that calls a partial to present the view.

Step 5: Build Contact Form with HTML

In Step 2, we created a GraphQL file named contacts/create, which corresponds to the relative path of the file in graphql directory. Now, we'll build an HTML form to allow users to submit their contact request information. Create a new page in app/views/pages/contacts/new.liquid with the following content:

<form action="/contacts" method="post">
  <input type="hidden" name="authenticity_token" value="{{ context.authenticity_token }}" />
  <label for="name">Name</label>
  <input name="contact[name]" id="name" type="text">

  <label for="email">Email</label>
  <input name="contact[email]" id="email" type="email">

  <label for="description">Description</label>
  <textarea name="contact[description]" id="description"></textarea>


Each form field is marked as required to ensure that users provide the necessary information before submitting the form.

This piece of code ensures that the contact form is both functional and secure, integrating with the previously defined GraphQL mutation to handle the submission and persistence of contact data.


Authenticity token is used to prevent Cross-Site Request Forgery (CSRF) attacks. It's included to ensure that form submissions are coming from authenticated users. If you don't include it, users might face session invalidation issues after submitting the form. You can learn more about it in chapter about

Step 6: Invoking GraphQL Server-Side

In the previous step, we created a form that, upon submission, triggers a POST request to the /contacts endpoint. The form values will be accessible via context.params. Since we namespaced all input names, the values submitted by the user will be available through context.params.contact. For example, context.params.contact.name will return the value entered by the user.

First, we need to create a page that will be invoked for the form submission:

slug: contacts
method: post

{% liquid
  graphql g = "contacts/create", name: context.params.contact.name, email: context.params.contact.email, description: context.params.contact.description
  assign url = "/contacts/" | append: g.record_create.id
  redirect_to url

After submitting the form from Step 5, the browser will send a POST request to /contacts. platformOS, via the GraphQL Liquid Tag, will invoke the contacts/create GraphQL mutation using the parameter mapping that was defined (e.g., the GraphQL email argument will have a value equal to context.params.contact.email).

Once the GraphQL mutation is invoked, the user will be redirected to /contacts/<id>, where id is the value of the newly created record.


Providing manual mapping for each argument in GraphQL can be cumbersome. That's why there's a convenient args argument available. You can achieve the same result by using graphql g =

To confirm that everything works as intended, navigate to /contacts/new in your instance, complete the form, and submit it. You should be redirected to the show Page defined in Step 4, where you can review the information you submitted in the form. Furthermore, the new entry should be visible on the /contacts page.


In practical scenarios, it

Step 7: Delete a Record

In order to remove record from the database, use record_delete GraphQL mutation.

Step 8: Update a Record

In order to update a record, use record_update GraphQL mutation. Only the properties explicitly defined in the GraphQL mutation will be updated. The properties you do not define as arguments will stay untouched.

Next steps

Congratulations! You have learned how to build forms and persist user input using Records. You can now explore our reference documentation for Properties and Tables:


We are always happy to help with any questions you may have.

contact us