A complete guide for "userActions" (and the "Action button" widget)

Grist has an extremely useful feature: User Actions.

Unfortunately, there is almost no documentation on this subject. This post is my attempt to solve this problem, and empower more people (both user and developers) to use them.

If you are just here to know how to use the Action button widget, feel free to jump to the end.

The problem solved

In a typical API, the features are split into multiple functions, and each of them have different arguments. For example, grist.docApi.fetchTable and grist.docApi.listTables.

Now, let’s imagine that you want to create a very flexible widget, where the user can define actions to create, update or delete data from tables. In this situation, you want the user to have the most control possible. And the classic API design does not work, because the user can’t write the functions.

The solution is to Serialize the commands. This means the user can write commands, these commands get converted to text (json), are received by your widget, and are forwarded to the grist engine.

This brings another superpower: even if the core is in python and the widgets in js, you can communicate easily between the 2. You can even store them in a Grist table !

This is what userActions are: json commands you can pass between python and js.

Reference

There is no clean documentation for userActions yet. I will try to share everything I know here, but it would be still nice to have an official documentation provided by Grist.

In the source code, the user actions are defined here:

Here are a few examples (in python) to understand the structure:

add_few_lines = [
    ["AddRecord", "Table1", None, {"Name": "Alice" , "Age": 20}],
    ["AddRecord", "Table1", None, {"Name": "Bob"   , "Age": 21}],
    ["AddRecord", "Table1", None, {"Name": "Charly", "Age": 22}],
]
delete_line = [
    ["RemoveRecord", "Table1", 18] # delete the line with id=18
]

The commands are a list of “actions”, and each action is a list with 3 or 4 elements such that:

  • element 0 is the name of the command
  • element 1 is the name (id) of the table
  • element 2 is the id of the record (the line, corresponds to $id)
  • element 3 is some data, often a dictionary

Inside a widget, you can apply the actions with the function docApi.applyUserActions.

Let’s say you want to apply a command that the user put inside the table:

grist.onRecord(record => {
    const actions = record["actions"];
    docApi.applyUserActions(actions)
})

(Note that the widget needs the “full document” access to do this)

See the next session to see examples of user actions.

Mastering actions (and the Action Button widget)

The main way people use actions is through the Action Button builtin widget.

Below are examples of formulas you can copy-paste (and adapt :wink: ) inside Grist to execute common actions. Make sure you change the name of the table given as an example.

add a new line

actions = [["AddRecord", "Table1", None, {"Col1": "a", "Col2": "b"}]]
return {"actions": actions, "button": "Add new line", "description": ""}

remove the current line

actions = [["RemoveRecord", "Table1", $id]]
return {"actions": actions, "button": "Delete current line", "description": ""}

(make sure your “button action” widget has SELECT BY configured)

Duplicate current line

(beware, this will try to duplicate the formula column)

special_cols = ["id", "manualSort", "#lookup#"]
col_names = [x for x in rec._table.all_columns if x not in special_cols]
actions = [["AddRecord", "Table1", None, {c: getattr(rec, c) for c in col_names}]]
actions = [["RemoveRecord", "Table1", $id]]
return {"actions": actions, "button": "Delete current line", "description": ""}

duplicate all records

records = Table1.lookupRecords()
bulk_col_values = {
  "Col1": [r.Col1 for r in records],
  "Col1": [r.Col2 for r in records]
}
actions = [("BulkAddRecord", "Table1", [None for _ in records], bulk_col_values)]
return {"actions": actions, "button": "Duplicate all records", "description": ""}

delete all lines

rec_ids = Table1.lookupRecords().id

actions = [["BulkRemoveRecord", "Table1", rec_ids]]
return {"actions": actions, "button": "Empty table", "description": ""}

Fill missing records from another table

This one is more complex.

Let’s say you have a table ClassicBooks with books that are very popular.

You have a table Items with physical books, that are located in Libraries.

Now, you create a new library: you would like to add to the Items table a copy of each book. But if the book already exist, you don’t want to add it.

Here is how you do it:

existing = Items.lookupRecords(Library=$id).Book
existing_or_empty = existing.id if existing else [] # weird grist bug
to_add = [r for r in ClassicalBooks.all if r.id not in existing_or_empty]
return [("AddRecord", "Items", None, {"Library": $id, "Book": r.id}) for r in to_add]

Note: you may not need line 2. It was required because of a bug in Grist (github issue here), but it has been fixed recently.

Conclusion

I hope this post will be useful to you, both if you’re a developer or if you’re a grist enjoyer who likes buttons.

I stole some ideas to these posts, if you want to have more contexts and more ideas:

11 Likes

Thanks for putting this together — this thread was a very good effort and almost the only usable starting point for the action button widget I could find anywhere, official docs included. :clap:t2:

A good mention to French Grist community for a similar documentation.

I’ve been going deep on the action-button pattern myself over the past while (cross-checking against useractions.py and DocActions.ts directly to fill in the gaps this guide flags), and it left me with a question about where this knowledge should actually live long-term:

  • Is there any plan (official or community) to turn this into proper documentation? e.g. a page under Grist’s own help center, alongside the other custom-widget docs
  • Would a structured “skill” (a packaged, machine-readable version of this guide an AI assistant could load automatically when someone asks it to write a Grist button) be something the community/maintainers would find valuable? Where should it fit?

I’m curious what’s the button’s actual roadmap looks like?

The button give user action to live!

So what next?