diff --git a/fr/index.html b/fr/index.html index bc7d0cb0b..4ee691878 100644 --- a/fr/index.html +++ b/fr/index.html @@ -1065,5 +1065,5 @@
On all plans, the number of documents is not limited.
To prevent accidental abuse of the system by automation -tools, team sites may be limited to 1000 documents. If you encounter such a limit for legitimate +tools, team sites may be limited to 1,000 documents. If you encounter such a limit for legitimate use, please contact support to increase it.
-Older free plans had a limit of ten documents. Learn more about legacy limits.
+Older free plans had a limit of ten documents. Learn more about legacy limits.
For team sites on all plans, there is no limit on the number of team members that may be added to the site. For paid plans, the number of team members determines the price. See our pricing page for details.
Team members added to your team site may inherit access to workspaces or documents @@ -970,18 +975,18 @@
On the Free plan, documents have a limit of 5,000 rows.
-On the Pro plan, documents may have up to 100,000 rows. This is a rule of thumb. The actual limit depends -also on the number of tables, columns, and the average size of data in each cell. One way to -estimate it is to measure the size of the data when it is in CSV format: the limit is around 20MB -in this format. For example, a document with 200,000 rows and 12 numeric columns would reach that.
+On the Free plan, documents have a limit of 5,000 rows. The limit for Pro and Business plans is 100,000 and 150,000 rows, respectively.
+Documents are also subject to data size limits, as described below.
+There is a hard limit to a document’s total data size, determined as the row limit multiplied by 2KB. This means that documents on the Free plan have a data size limit of 10MB, with Pro and Business plan documents having limits of 200MB and 300MB, respectively. This value corresponds approximately to the size of the data in CSV format. You can see a document’s current data size on the ‘Raw Data’ page.
+For memory and performance reasons, there’s a recommended data size limit of 20MB. Documents beyond 20MB may slow down or run into memory limits depending on their complexity. As an example, a document with 100,000 rows and 24 numeric columns would reach this recommended limit. To help optimize formulas on large documents, you can use the built-in formula timer.
Attachments are counted separately. Attachments plus data in a single document are limited to 1GB on all plans.
-Uploads are limited to 50MB, both for attachments and to import data. Note that an import within this limit may result in a document that exceeds the document size limit, in which case the upload is still likely to fail.
Free plans are limited to 5,000 API calls per document per day. Pro plans raise the limit to 40,000 calls per document per day.
+Free plans are limited to 5,000 API calls per document per day. Pro and Business plans raise the limit to 40,000 and 60,000 calls per document per day, respectively.
Free plans may be rate limited to 5 API requests per second per document. The team plan does not impose such a rate limit.
Separately, there is a concurrency limit of 10 for all plans: if 10 authorized API requests are @@ -1004,7 +1009,7 @@
To determine if you’re on a legacy personal site, click on your site name (@your-name) in the top left. Personal sites on the legacy plan will say “Personal Site (Legacy)” in the dropdown menu.
-On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page.
+On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page.
from other elements using blank lines. Configuring submission options # You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel. Publishing your form # Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error. Form submissions # After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form"},{"location":"widget-form/#page-widget-form","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The form widget allows you to collect data in a form view which populates your Grist data table upon submission.","title":"Page widget: Form"},{"location":"widget-form/#setting-up-your-data","text":"Create a table containing the columns of data you wish to populate via form.","title":"Setting up your data"},{"location":"widget-form/#creating-your-form","text":"Add a form widget from the \u201cAdd New\u201d menu. Select the data table you wish to populate with form data. Then, customize the form to your heart\u2019s desire! By default, the form view will include elements for headers and descriptions as well as all columns (fields) from the underlying data table.","title":"Creating your form"},{"location":"widget-form/#adding-and-removing-elements","text":"To add additional form elements, click the + icon at the bottom of the form. From the menu, you can add the following elements: New Question: Select a column type to create a new field. \u201c\u2022\u2022\u2022 More >\u201d will open an expanded menu listing all column types. Adding a new question will add a new column to the underlying data table. Unmapped Fields: Lists any hidden fields from the underlying data table. Building Blocks: Customize further by adding these additional elements! You can remove any element from the form by hovering over the object and clicking the trash icon to delete. You can hide any uneccessary fields from the form by hovering over the object and clicking the x icon.","title":"Adding and removing elements"},{"location":"widget-form/#configuring-fields","text":"You can provide alternative titles for your form fields, rather than use the same column name from the underlying data table. For example, on our form, we have a toggle that is titled \u201cMay we contact you?\u201d. In the data table, this column is labeled \u201cOk to Contact?\u201d. Field titles can be configured under the \u201cField\u201d tab of the creator panel. To make a form field required, check the box next to \u201cRequired field\u201d. If a user attempts to submit a form without filling in the required field, they will get an alert to fill out the field.","title":"Configuring fields"},{"location":"widget-form/#configuring-building-blocks","text":"Header and Paragraph building blocks can be edited either directly in the block or from the creator panel. In the creator panel, you have text alignment options available. For additional formatting, both elements allow the use of Markdown formatting. For help on Markdown formatting, check out the Markdown Guide . HTML Formatting HTML tags can be used in Markdown-formatted text. Be sure to separate block-level HTML elements like
from other elements using blank lines.","title":"Configuring building blocks"},{"location":"widget-form/#configuring-submission-options","text":"You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel.","title":"Configuring submission options"},{"location":"widget-form/#publishing-your-form","text":"Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error.","title":"Publishing your form"},{"location":"widget-form/#form-submissions","text":"After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form submissions"},{"location":"widget-chart/","text":"Page widget: Chart # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist supports several chart types to help you visualize your data easily. Charts may be used to plot a regular table of data, a linked widget (as described in Linking widgets ), or a summary table (as described in Summary tables ). The most common chart types are illustrated here: Chart types # Each chart type plots one or several data series. Select the series to plot by clicking the green \u2018Add Series\u2019 text in the creator panel . In the area above the \u201cSeries\u201d section of the creator panel, you may configure the x-axis for most charts, or the labels for pie and donut charts. Bar Chart # Needs an x-axis and at least one series to plot along the y-axis. Additional series create additional bars at each point on the x-axis. To stack series onto the same bar, select the \u201cStack series\u201d checkbox. Line Chart # Needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines. Pie Chart # Needs pie slice labels and one series for the pie slice sizes. Area Chart # Similar to a line chart, needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines. Scatter Plot # Needs a label and two or more series. The label applies to the points. The series apply the X and Y values for each point, respectively. Additional series specify Y values for additional sets of points. Kaplan-Meier Plot # The Kaplan-Meier Plot is useful for certain studies, and needs one label and one series. The label applies to the lines being plotted. The series gives a survival time or time-to-failure of that point. The plot shows the survival times on the X axis, and the number of points that survive at that time on the Y axis. Chart options # A number of chart options are available, some of them specific to certain chart types. Split Series : When checked, an extra series is to be selected under the \u2018Split Series\u2019 dropdown. The series should contain a group label for each data point. All points with the same group value are plotted as a separate line. For example: Invert Y-axis : When checked the Y axis is flipped, with smaller values above and larger values below. Connect gaps [for Line Charts only]: When checked, gaps caused by missing values are connected by connecting neighboring points. The \u201cShow Markers\u201d option described next can be used to keep a visual cue for which points are present. Show markers [for Line Charts only]: When checked, each point on the line is marked additionally by a small circle. See the example for Split Series above. Stack series [for Line and Bar Charts]: When checked, split series will be stacked, rather than shown separately, giving a total for your selected series. In this example, we can see the total revenue for each month across all three departments. Note that \u2018Split series\u2019 must be checked in order to select multiple series to stack. Error bars [for Line and Bar Charts]: When set to \u201cSymmetric\u201d, each Y series should be followed by a series for the length of the error bars to show. When set to \u201cAbove+Below\u201d, each Y series should be followed by two series, one for the top error bars, and one for the bottom. In the example here, \u201cSplit Series\u201d is checked. So the series selected from the Split Series dropdown (\u201cCell Line\u201d), specifies how to group the data into lines. The series selected under the X-Axis dropdown (\u201cLog[Drug], uM\u201d), specifies our values along the x-axis. Our first series in the Series list (\u201c% Viable Cells\u201d), specifies our y-axis values, and the series that follows that (\u201cSD\u201d), specifies the error bars for those Y values.","title":"Chart"},{"location":"widget-chart/#page-widget-chart","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist supports several chart types to help you visualize your data easily. Charts may be used to plot a regular table of data, a linked widget (as described in Linking widgets ), or a summary table (as described in Summary tables ). The most common chart types are illustrated here:","title":"Page widget: Chart"},{"location":"widget-chart/#chart-types","text":"Each chart type plots one or several data series. Select the series to plot by clicking the green \u2018Add Series\u2019 text in the creator panel . In the area above the \u201cSeries\u201d section of the creator panel, you may configure the x-axis for most charts, or the labels for pie and donut charts.","title":"Chart types"},{"location":"widget-chart/#bar-chart","text":"Needs an x-axis and at least one series to plot along the y-axis. Additional series create additional bars at each point on the x-axis. To stack series onto the same bar, select the \u201cStack series\u201d checkbox.","title":"Bar Chart"},{"location":"widget-chart/#line-chart","text":"Needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines.","title":"Line Chart"},{"location":"widget-chart/#pie-chart","text":"Needs pie slice labels and one series for the pie slice sizes.","title":"Pie Chart"},{"location":"widget-chart/#area-chart","text":"Similar to a line chart, needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines.","title":"Area Chart"},{"location":"widget-chart/#scatter-plot","text":"Needs a label and two or more series. The label applies to the points. The series apply the X and Y values for each point, respectively. Additional series specify Y values for additional sets of points.","title":"Scatter Plot"},{"location":"widget-chart/#kaplan-meier-plot","text":"The Kaplan-Meier Plot is useful for certain studies, and needs one label and one series. The label applies to the lines being plotted. The series gives a survival time or time-to-failure of that point. The plot shows the survival times on the X axis, and the number of points that survive at that time on the Y axis.","title":"Kaplan-Meier Plot"},{"location":"widget-chart/#chart-options","text":"A number of chart options are available, some of them specific to certain chart types. Split Series : When checked, an extra series is to be selected under the \u2018Split Series\u2019 dropdown. The series should contain a group label for each data point. All points with the same group value are plotted as a separate line. For example: Invert Y-axis : When checked the Y axis is flipped, with smaller values above and larger values below. Connect gaps [for Line Charts only]: When checked, gaps caused by missing values are connected by connecting neighboring points. The \u201cShow Markers\u201d option described next can be used to keep a visual cue for which points are present. Show markers [for Line Charts only]: When checked, each point on the line is marked additionally by a small circle. See the example for Split Series above. Stack series [for Line and Bar Charts]: When checked, split series will be stacked, rather than shown separately, giving a total for your selected series. In this example, we can see the total revenue for each month across all three departments. Note that \u2018Split series\u2019 must be checked in order to select multiple series to stack. Error bars [for Line and Bar Charts]: When set to \u201cSymmetric\u201d, each Y series should be followed by a series for the length of the error bars to show. When set to \u201cAbove+Below\u201d, each Y series should be followed by two series, one for the top error bars, and one for the bottom. In the example here, \u201cSplit Series\u201d is checked. So the series selected from the Split Series dropdown (\u201cCell Line\u201d), specifies how to group the data into lines. The series selected under the X-Axis dropdown (\u201cLog[Drug], uM\u201d), specifies our values along the x-axis. Our first series in the Series list (\u201c% Viable Cells\u201d), specifies our y-axis values, and the series that follows that (\u201cSD\u201d), specifies the error bars for those Y values.","title":"Chart options"},{"location":"widget-calendar/","text":"Page widget: Calendar # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The calendar widget allows viewing data in a calendar view. In Grist, event data is stored in a data table. Then, the calendar widget can be configured to display that data. Setting up your data # In your data table, you will need two columns of data, with the option to add three additional data columns: Title : Text column, containing the title of your event. Start Date : Date or DateTime column, containing the date, or date and time, that the event begins. (Optional) End Date : Date or DateTime column, containing the date, or date and time, that the event ends. (Optional) Is All Day? : Toggle column, noting if an event is all day long. (Optional) Type : Choice or Choice List column, containing the event category and styling. Configuring the calendar # Add a calendar widget from the \u2018Add New\u2019 menu. Select the table containing your event data. Configure the widget by selecting the columns in your data table that contain Start Date, End Date (optional), Is All Day? (optional), Title and Type (optional). Adding a new event # You can add a new event by double-clicking the start time for the event in the calendar widget. In the popup, you can add a title for the event and modify the start and end time. You also have the option to mark the event as \u2018all day\u2019. You can also modify the start and end time of an event by clicking and dragging the event directly on the calendar. To modify an event\u2019s start time, click and drag from the middle of the event. When modifying start time, the duration of the event will remain the same. To modify an event\u2019s end time, click and drag from the bottom of the event. You\u2019ll notice that the icon is slightly different from the icon that appears when modifying start time. Any changes to start and end time will be made to the underlying data table so your data will always be consistent! Linking event details # It might be useful to see more event details in a table or card widget. This example will walk through a card widget. To create a linked view, add a new type of widget such as a table or a card, and select the same data table. Under \u2018Select By\u2019, select the calendar widget. Then, add to page. Now, when you click on an event in the calendar widget, the linked widget will update to show the details for the selected event. Collapsing widgets Drag the linked widget into the attic at the top of the page to collapse. The widget will still be linked but will take up less space on the page! Simply click the box to expand and view linked record details. Read more on Custom Layouts . Deleting an event # To delete an event, double-click the event in the calendar then select \u2018delete\u2019 in the popup.","title":"Calendar"},{"location":"widget-calendar/#page-widget-calendar","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The calendar widget allows viewing data in a calendar view. In Grist, event data is stored in a data table. Then, the calendar widget can be configured to display that data.","title":"Page widget: Calendar"},{"location":"widget-calendar/#setting-up-your-data","text":"In your data table, you will need two columns of data, with the option to add three additional data columns: Title : Text column, containing the title of your event. Start Date : Date or DateTime column, containing the date, or date and time, that the event begins. (Optional) End Date : Date or DateTime column, containing the date, or date and time, that the event ends. (Optional) Is All Day? : Toggle column, noting if an event is all day long. (Optional) Type : Choice or Choice List column, containing the event category and styling.","title":"Setting up your data"},{"location":"widget-calendar/#configuring-the-calendar","text":"Add a calendar widget from the \u2018Add New\u2019 menu. Select the table containing your event data. Configure the widget by selecting the columns in your data table that contain Start Date, End Date (optional), Is All Day? (optional), Title and Type (optional).","title":"Configuring the calendar"},{"location":"widget-calendar/#adding-a-new-event","text":"You can add a new event by double-clicking the start time for the event in the calendar widget. In the popup, you can add a title for the event and modify the start and end time. You also have the option to mark the event as \u2018all day\u2019. You can also modify the start and end time of an event by clicking and dragging the event directly on the calendar. To modify an event\u2019s start time, click and drag from the middle of the event. When modifying start time, the duration of the event will remain the same. To modify an event\u2019s end time, click and drag from the bottom of the event. You\u2019ll notice that the icon is slightly different from the icon that appears when modifying start time. Any changes to start and end time will be made to the underlying data table so your data will always be consistent!","title":"Adding a new event"},{"location":"widget-calendar/#linking-event-details","text":"It might be useful to see more event details in a table or card widget. This example will walk through a card widget. To create a linked view, add a new type of widget such as a table or a card, and select the same data table. Under \u2018Select By\u2019, select the calendar widget. Then, add to page. Now, when you click on an event in the calendar widget, the linked widget will update to show the details for the selected event. Collapsing widgets Drag the linked widget into the attic at the top of the page to collapse. The widget will still be linked but will take up less space on the page! Simply click the box to expand and view linked record details. Read more on Custom Layouts .","title":"Linking event details"},{"location":"widget-calendar/#deleting-an-event","text":"To delete an event, double-click the event in the calendar then select \u2018delete\u2019 in the popup.","title":"Deleting an event"},{"location":"widget-custom/","text":"Page widget: Custom # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The Custom widget allows a user to insert almost anything in their document. To create a custom widget currently requires knowledge of web development, and access to a public web server (for example, GitHub Pages). A good use for custom widgets is to view records or tables in new ways. Using Grist as your data model and modern HTML/CSS/JS as your view is very powerful. Minimal example # To demonstrate to a web developer how custom widgets work, there is a minimal working example at: https://public.getgrist.com/911KcgKA95oQ/Minimal-Custom-Widget/m/fork The example shows a table with some random data (names for pets), and two custom widgets, one showing the selected row in the table as JSON, and the other showing all rows of the table as JSON. If you change data in the table, or move the cursor, the custom widgets update as appropriate. The source code for the widgets is at: https://github.com/gristlabs/grist-widget/tree/master/inspect It is stripped down to the essentials. Here is the full source code of the onRecord widget that shows one row of data:
Waiting for data...The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support. Adding a custom widget # To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html Access level # When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget. Invoice example # The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord . Creating a custom widget # As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API. Column mapping # Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping. Widget options # If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen. Custom Widget linking # Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'}); Premade Custom Widgets # All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown. Advanced Charts # The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration. Copy to clipboard # Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Dropbox Embedder # View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template. Grist Video Player # Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget. HTML Viewer # The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Image Viewer # View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar ! JupyterLite Notebook # This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
Waiting for data...The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support.","title":"Minimal example"},{"location":"widget-custom/#adding-a-custom-widget","text":"To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html","title":"Adding a custom widget"},{"location":"widget-custom/#access-level","text":"When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget.","title":"Access level"},{"location":"widget-custom/#invoice-example","text":"The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord .","title":"Invoice example"},{"location":"widget-custom/#creating-a-custom-widget","text":"As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API.","title":"Creating a custom widget"},{"location":"widget-custom/#column-mapping","text":"Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping.","title":"Column mapping"},{"location":"widget-custom/#widget-options","text":"If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen.","title":"Widget options"},{"location":"widget-custom/#custom-widget-linking","text":"Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'});","title":"Custom Widget linking"},{"location":"widget-custom/#premade-custom-widgets","text":"All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown.","title":"Premade Custom Widgets"},{"location":"widget-custom/#advanced-charts","text":"The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration.","title":"Advanced Charts"},{"location":"widget-custom/#copy-to-clipboard","text":"Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"Copy to clipboard"},{"location":"widget-custom/#dropbox-embedder","text":"View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template.","title":"Dropbox Embedder"},{"location":"widget-custom/#grist-video-player","text":"Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget.","title":"Grist Video Player"},{"location":"widget-custom/#html-viewer","text":"The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"HTML Viewer"},{"location":"widget-custom/#image-viewer","text":"View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar !","title":"Image Viewer"},{"location":"widget-custom/#jupyterlite-notebook","text":"This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
DateTime:America/New_York
, Ref:Users
\",\"enum\":[\"Any\",\"Text\",\"Numeric\",\"Int\",\"Bool\",\"Date\",\"DateTime:$A + Table1.lookupOne(B=$B)
\"},\"isFormula\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column is a formula column. Use \\\"false\\\" for trigger formula column.\"},\"widgetOptions\":{\"type\":\"string\",\"description\":\"A JSON object with widget options, e.g.: {\\\"choices\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"alignment\\\": \\\"right\\\"}
\"},\"untieColIdFromLabel\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column label should not be used as the column identifier. Use \\\"false\\\" to use the label as the identifier.\"},\"recalcWhen\":{\"type\":\"integer\",\"description\":\"A number indicating when the column should be recalculated. [2, 3]
\"}}}]},\"GetFields\":{\"allOf\":[{\"$ref\":\"#/components/schemas/Fields\"},{\"type\":\"object\",\"properties\":{\"recalcDeps\":{\"type\":\"array\",\"items\":{\"type\":\"integer\"},\"description\":\"An array of column identifiers (colRefs) that this column depends on, prefixed with \\\"L\\\" constant. If any of these columns change, the column will be recalculated. E.g.: [\\\"L\\\", 2, 3]
\"},\"colRef\":{\"type\":\"integer\",\"description\":\"Column reference, e.g.: 2
\"}}}]},\"RowIds\":{\"type\":\"array\",\"example\":[101,102,103],\"items\":{\"type\":\"integer\"}},\"DocParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Competitive Analysis\"},\"isPinned\":{\"type\":\"boolean\",\"example\":false}}},\"WorkspaceParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Retreat Docs\"}}},\"OrgParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"ACME Unlimited\"}}},\"OrgAccessRead\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"OrgAccessWrite\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"WorkspaceAccessRead\":{\"type\":\"object\",\"required\":[\"maxInheritedRole\",\"users\"],\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"},\"parentAccess\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"WorkspaceAccessWrite\":{\"type\":\"object\",\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"DocAccessWrite\":{\"$ref\":\"#/components/schemas/WorkspaceAccessWrite\"},\"DocAccessRead\":{\"$ref\":\"#/components/schemas/WorkspaceAccessRead\"},\"AttachmentUpload\":{\"type\":\"object\",\"properties\":{\"upload\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"format\":\"binary\"}}}},\"AttachmentId\":{\"type\":\"number\",\"description\":\"An integer ID\"},\"AttachmentMetadata\":{\"type\":\"object\",\"properties\":{\"fileName\":{\"type\":\"string\",\"example\":\"logo.png\"},\"fileSize\":{\"type\":\"number\",\"example\":12345},\"timeUploaded\":{\"type\":\"string\",\"example\":\"2020-02-13T12:17:19.000Z\"}}},\"AttachmentMetadataList\":{\"type\":\"object\",\"required\":[\"records\"],\"properties\":{\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"fields\"],\"properties\":{\"id\":{\"type\":\"number\",\"example\":1},\"fields\":{\"$ref\":\"#/components/schemas/AttachmentMetadata\"}}}}}},\"SqlResultSet\":{\"type\":\"object\",\"required\":[\"statement\",\"records\"],\"properties\":{\"statement\":{\"type\":\"string\",\"description\":\"A copy of the SQL statement.\",\"example\":\"select * from Pets ...\"},\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"object\"}}},\"example\":[{\"fields\":{\"id\":1,\"pet\":\"cat\",\"popularity\":67}},{\"fields\":{\"id\":2,\"pet\":\"dog\",\"popularity\":95}}]}}},\"TableSchemaResult\":{\"type\":\"object\",\"required\":[\"name\",\"title\",\"schema\"],\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"The ID (technical name) of the table\"},\"title\":{\"type\":\"string\",\"description\":\"The human readable name of the table\"},\"path\":{\"type\":\"string\",\"description\":\"The URL to download the CSV\",\"example\":\"https://getgrist.com/o/docs/api/docs/ID/download/csv?tableId=Table1&....\"},\"format\":{\"type\":\"string\",\"enum\":[\"csv\"]},\"mediatype\":{\"type\":\"string\",\"enum\":[\"text/csv\"]},\"encoding\":{\"type\":\"string\",\"enum\":[\"utf-8\"]},\"dialect\":{\"$ref\":\"#/components/schemas/csv-dialect\"},\"schema\":{\"$ref\":\"#/components/schemas/table-schema\"}}},\"csv-dialect\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"CSV Dialect\",\"description\":\"The CSV dialect descriptor.\",\"type\":[\"string\",\"object\"],\"required\":[\"delimiter\",\"doubleQuote\"],\"properties\":{\"csvddfVersion\":{\"title\":\"CSV Dialect schema version\",\"description\":\"A number to indicate the schema version of CSV Dialect. Version 1.0 was named CSV Dialect Description Format and used different field names.\",\"type\":\"number\",\"default\":1.2,\"examples:\":[\"{\\n \\\"csvddfVersion\\\": \\\"1.2\\\"\\n}\\n\"]},\"delimiter\":{\"title\":\"Delimiter\",\"description\":\"A character sequence to use as the field separator.\",\"type\":\"string\",\"default\":\",\",\"examples\":[\"{\\n \\\"delimiter\\\": \\\",\\\"\\n}\\n\",\"{\\n \\\"delimiter\\\": \\\";\\\"\\n}\\n\"]},\"doubleQuote\":{\"title\":\"Double Quote\",\"description\":\"Specifies the handling of quotes inside fields.\",\"context\":\"If Double Quote is set to true, two consecutive quotes must be interpreted as one.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"doubleQuote\\\": true\\n}\\n\"]},\"lineTerminator\":{\"title\":\"Line Terminator\",\"description\":\"Specifies the character sequence that must be used to terminate rows.\",\"type\":\"string\",\"default\":\"\\r\\n\",\"examples\":[\"{\\n \\\"lineTerminator\\\": \\\"\\\\r\\\\n\\\"\\n}\\n\",\"{\\n \\\"lineTerminator\\\": \\\"\\\\n\\\"\\n}\\n\"]},\"nullSequence\":{\"title\":\"Null Sequence\",\"description\":\"Specifies the null sequence, for example, `\\\\N`.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"nullSequence\\\": \\\"\\\\N\\\"\\n}\\n\"]},\"quoteChar\":{\"title\":\"Quote Character\",\"description\":\"Specifies a one-character string to use as the quoting character.\",\"type\":\"string\",\"default\":\"\\\"\",\"examples\":[\"{\\n \\\"quoteChar\\\": \\\"'\\\"\\n}\\n\"]},\"escapeChar\":{\"title\":\"Escape Character\",\"description\":\"Specifies a one-character string to use as the escape character.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"escapeChar\\\": \\\"\\\\\\\\\\\"\\n}\\n\"]},\"skipInitialSpace\":{\"title\":\"Skip Initial Space\",\"description\":\"Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"skipInitialSpace\\\": true\\n}\\n\"]},\"header\":{\"title\":\"Header\",\"description\":\"Specifies if the file includes a header row, always as the first row in the file.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"header\\\": true\\n}\\n\"]},\"commentChar\":{\"title\":\"Comment Character\",\"description\":\"Specifies that any row beginning with this one-character string, without preceeding whitespace, causes the entire line to be ignored.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"commentChar\\\": \\\"#\\\"\\n}\\n\"]},\"caseSensitiveHeader\":{\"title\":\"Case Sensitive Header\",\"description\":\"Specifies if the case of headers is meaningful.\",\"context\":\"Use of case in source CSV files is not always an intentional decision. For example, should \\\"CAT\\\" and \\\"Cat\\\" be considered to have the same meaning.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"caseSensitiveHeader\\\": true\\n}\\n\"]}},\"examples\":[\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\";\\\"\\n }\\n}\\n\",\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\"\\\\t\\\",\\n \\\"quoteChar\\\": \\\"'\\\",\\n \\\"commentChar\\\": \\\"#\\\"\\n }\\n}\\n\"]},\"table-schema\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Table Schema\",\"description\":\"A Table Schema for this resource, compliant with the [Table Schema](./tableschema/) specification.\",\"type\":[\"string\",\"object\"],\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Field\",\"type\":\"object\",\"oneOf\":[{\"type\":\"object\",\"title\":\"String Field\",\"description\":\"The field contains strings, that is, sequences of characters.\",\"required\":[\"name\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `string`.\",\"enum\":[\"string\"]},\"format\":{\"description\":\"The format keyword options for `string` are `default`, `email`, `uri`, `binary`, and `uuid`.\",\"context\":\"The following `format` options are supported:\\n * **default**: any valid string.\\n * **email**: A valid email address.\\n * **uri**: A valid URI.\\n * **binary**: A base64 encoded string representing binary data.\\n * **uuid**: A string that is a uuid.\",\"enum\":[\"default\",\"email\",\"uri\",\"binary\",\"uuid\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `string` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"pattern\":{\"type\":\"string\",\"description\":\"A regular expression pattern to test each value of the property against, where a truthy response indicates validity.\",\"context\":\"Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs).\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"constraints\\\": {\\n \\\"minLength\\\": 3,\\n \\\"maxLength\\\": 35\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Number Field\",\"description\":\"The field contains numbers of any kind including decimals.\",\"context\":\"The lexical formatting follows that of decimal in [XMLSchema](https://www.w3.org/TR/xmlschema-2/#decimal): a non-empty finite-length sequence of decimal digits separated by a period as a decimal indicator. An optional leading sign is allowed. If the sign is omitted, '+' is assumed. Leading and trailing zeroes are optional. If the fractional part is zero, the period and following zero(es) can be omitted. For example: '-1.23', '12678967.543233', '+100000.00', '210'.\\n\\nThe following special string values are permitted (case does not need to be respected):\\n - NaN: not a number\\n - INF: positive infinity\\n - -INF: negative infinity\\n\\nA number `MAY` also have a trailing:\\n - exponent: this `MUST` consist of an E followed by an optional + or - sign followed by one or more decimal digits (0-9)\\n - percentage: the percentage sign: `%`. In conversion percentages should be divided by 100.\\n\\nIf both exponent and percentages are present the percentage `MUST` follow the exponent e.g. '53E10%' (equals 5.3).\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `number`.\",\"enum\":[\"number\"]},\"format\":{\"description\":\"There are no format keyword options for `number`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"decimalChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to represent a decimal point within the number. The default value is `.`.\"},\"groupChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to group digits within the number. The default value is `null`. A common value is `,` e.g. '100,000'.\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `number` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"number\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\",\\n \\\"constraints\\\": {\\n \\\"enum\\\": [ \\\"1.00\\\", \\\"1.50\\\", \\\"2.00\\\" ]\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Integer Field\",\"description\":\"The field contains integers - that is whole numbers.\",\"context\":\"Integer values are indicated in the standard way for any valid integer.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `integer`.\",\"enum\":[\"integer\"]},\"format\":{\"description\":\"There are no format keyword options for `integer`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `integer` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\",\\n \\\"constraints\\\": {\\n \\\"unique\\\": true,\\n \\\"minimum\\\": 100,\\n \\\"maximum\\\": 9999\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Field\",\"description\":\"The field contains temporal date values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `date`.\",\"enum\":[\"date\"]},\"format\":{\"description\":\"The format keyword options for `date` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string of YYYY-MM-DD.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `date` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": \\\"01-01-1900\\\"\\n }\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"format\\\": \\\"MM-DD-YYYY\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Time Field\",\"description\":\"The field contains temporal time values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `time`.\",\"enum\":[\"time\"]},\"format\":{\"description\":\"The format keyword options for `time` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for time.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `time` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\",\\n \\\"format\\\": \\\"any\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Time Field\",\"description\":\"The field contains temporal datetime values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `datetime`.\",\"enum\":[\"datetime\"]},\"format\":{\"description\":\"The format keyword options for `datetime` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for datetime.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `datetime` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\",\\n \\\"format\\\": \\\"default\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Field\",\"description\":\"A calendar year, being an integer with 4 digits. Equivalent to [gYear in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYear)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `year`.\",\"enum\":[\"year\"]},\"format\":{\"description\":\"There are no format keyword options for `year`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `year` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1970,\\n \\\"maximum\\\": 2003\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Month Field\",\"description\":\"A calendar year month, being an integer with 1 or 2 digits. Equivalent to [gYearMonth in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYearMonth)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `yearmonth`.\",\"enum\":[\"yearmonth\"]},\"format\":{\"description\":\"There are no format keyword options for `yearmonth`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `yearmonth` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1,\\n \\\"maximum\\\": 6\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Boolean Field\",\"description\":\"The field contains boolean (true/false) data.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `boolean`.\",\"enum\":[\"boolean\"]},\"format\":{\"description\":\"There are no format keyword options for `boolean`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"trueValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"true\",\"True\",\"TRUE\",\"1\"]},\"falseValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"false\",\"False\",\"FALSE\",\"0\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `boolean` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"boolean\"}}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"registered\\\",\\n \\\"type\\\": \\\"boolean\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Object Field\",\"description\":\"The field contains data which can be parsed as a valid JSON object.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `object`.\",\"enum\":[\"object\"]},\"format\":{\"description\":\"There are no format keyword options for `object`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `object` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"extra\\\"\\n \\\"type\\\": \\\"object\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoPoint Field\",\"description\":\"The field contains data describing a geographic point.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geopoint`.\",\"enum\":[\"geopoint\"]},\"format\":{\"description\":\"The format keyword options for `geopoint` are `default`,`array`, and `object`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A string of the pattern 'lon, lat', where `lon` is the longitude and `lat` is the latitude.\\n * **array**: An array of exactly two items, where each item is either a number, or a string parsable as a number, and the first item is `lon` and the second item is `lat`.\\n * **object**: A JSON object with exactly two keys, `lat` and `lon`\",\"notes\":[\"Implementations `MUST` strip all white space in the default format of `lon, lat`.\"],\"enum\":[\"default\",\"array\",\"object\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geopoint` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\",\\n \\\"format\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoJSON Field\",\"description\":\"The field contains a JSON object according to GeoJSON or TopoJSON\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geojson`.\",\"enum\":[\"geojson\"]},\"format\":{\"description\":\"The format keyword options for `geojson` are `default` and `topojson`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A geojson object as per the [GeoJSON spec](http://geojson.org/).\\n * **topojson**: A topojson object as per the [TopoJSON spec](https://github.com/topojson/topojson-specification/blob/master/README.md)\",\"enum\":[\"default\",\"topojson\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geojson` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\",\\n \\\"format\\\": \\\"topojson\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Array Field\",\"description\":\"The field contains data which can be parsed as a valid JSON array.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `array`.\",\"enum\":[\"array\"]},\"format\":{\"description\":\"There are no format keyword options for `array`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `array` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"options\\\"\\n \\\"type\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Duration Field\",\"description\":\"The field contains a duration of time.\",\"context\":\"The lexical representation for duration is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) extended format `PnYnMnDTnHnMnS`, where `nY` represents the number of years, `nM` the number of months, `nD` the number of days, 'T' is the date/time separator, `nH` the number of hours, `nM` the number of minutes and `nS` the number of seconds. The number of seconds can include decimal digits to arbitrary precision. Date and time elements including their designator may be omitted if their value is zero, and lower order elements may also be omitted for reduced precision. Here we follow the definition of [XML Schema duration datatype](http://www.w3.org/TR/xmlschema-2/#duration) directly and that definition is implicitly inlined here.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `duration`.\",\"enum\":[\"duration\"]},\"format\":{\"description\":\"There are no format keyword options for `duration`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `duration` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"period\\\"\\n \\\"type\\\": \\\"duration\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Any Field\",\"description\":\"Any value is accepted, including values that are not captured by the type/format/constraint requirements of the specification.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `any`.\",\"enum\":[\"any\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply to `any` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"notes\\\",\\n \\\"type\\\": \\\"any\\\"\\n\"]}]},\"description\":\"An `array` of Table Schema Field objects.\",\"examples\":[\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\"\\n }\\n ]\\n}\\n\",\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n },\\n {\\n \\\"name\\\": \\\"my-field-name-2\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n }\\n ]\\n}\\n\"]},\"primaryKey\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"string\"}],\"description\":\"A primary key is a field name or an array of field names, whose values `MUST` uniquely identify each row in the table.\",\"context\":\"Field name in the `primaryKey` `MUST` be unique, and `MUST` match a field name in the associated table. It is acceptable to have an array with a single value, indicating that the value of a single field is the primary key.\",\"examples\":[\"{\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n}\\n\",\"{\\n \\\"primaryKey\\\": [\\n \\\"first_name\\\",\\n \\\"last_name\\\"\\n ]\\n}\\n\"]},\"foreignKeys\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Foreign Key\",\"description\":\"Table Schema Foreign Key\",\"type\":\"object\",\"required\":[\"fields\",\"reference\"],\"oneOf\":[{\"properties\":{\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"minItems\":1,\"uniqueItems\":true,\"description\":\"Fields that make up the primary key.\"}},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"minItems\":1,\"uniqueItems\":true}}}}},{\"properties\":{\"fields\":{\"type\":\"string\",\"description\":\"Fields that make up the primary key.\"},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"string\"}}}}}]},\"examples\":[\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"the-resource\\\",\\n \\\"fields\\\": \\\"state_id\\\"\\n }\\n }\\n ]\\n}\\n\",\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"\\\",\\n \\\"fields\\\": \\\"id\\\"\\n }\\n }\\n ]\\n}\\n\"]},\"missingValues\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"default\":[\"\"],\"description\":\"Values that when encountered in the source, should be considered as `null`, 'not present', or 'blank' values.\",\"context\":\"Many datasets arrive with missing data values, either because a value was not collected or it never existed.\\nMissing values may be indicated simply by the value being empty in other cases a special value may have been used e.g. `-`, `NaN`, `0`, `-9999` etc.\\nThe `missingValues` property provides a way to indicate that these values should be interpreted as equivalent to null.\\n\\n`missingValues` are strings rather than being the data type of the particular field. This allows for comparison prior to casting and for fields to have missing value which are not of their type, for example a `number` field to have missing values indicated by `-`.\\n\\nThe default value of `missingValue` for a non-string type field is the empty string `''`. For string type fields there is no default for `missingValue` (for string fields the empty string `''` is a valid value and need not indicate null).\",\"examples\":[\"{\\n \\\"missingValues\\\": [\\n \\\"-\\\",\\n \\\"NaN\\\",\\n \\\"\\\"\\n ]\\n}\\n\",\"{\\n \\\"missingValues\\\": []\\n}\\n\"]}},\"examples\":[\"{\\n \\\"schema\\\": {\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"first_name\\\",\\n \\\"type\\\": \\\"string\\\"\\n \\\"constraints\\\": {\\n \\\"required\\\": true\\n }\\n },\\n {\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\"\\n },\\n ],\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n }\\n}\\n\"]}},\"parameters\":{\"filterQueryParam\":{\"in\":\"query\",\"name\":\"filter\",\"schema\":{\"type\":\"string\"},\"description\":\"This is a JSON object mapping column names to arrays of allowed values. For example, to filter column `pet` for values `cat` and `dog`, the filter would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}`. JSON contains characters that are not safe to place in a URL, so it is important to url-encode them. For this example, the url-encoding is `%7B%22pet%22%3A%20%5B%22cat%22%2C%20%22dog%22%5D%7D`. See https://rosettacode.org/wiki/URL_encoding for how to url-encode a string, or https://www.urlencoder.org/ to try some examples. Multiple columns can be filtered. For example the filter for `pet` being either `cat` or `dog`, AND `size` being either `tiny` or `outrageously small`, would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"size\\\": [\\\"tiny\\\", \\\"outrageously small\\\"]}`.\",\"example\":\"{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}\",\"required\":false},\"sortQueryParam\":{\"in\":\"query\",\"name\":\"sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Order in which to return results. If a single column name is given (e.g. `pet`), results are placed in ascending order of values in that column. To get results in an order that was previously prepared manually in Grist, use the special `manualSort` column name. Multiple columns can be specified, separated by commas (e.g. `pet,age`). For descending order, prefix a column name with a `-` character (e.g. `pet,-age`). To include additional sorting options append them after a colon (e.g. `pet,-age:naturalSort;emptyFirst,owner`). Available options are: `choiceOrder`, `naturalSort`, `emptyFirst`. Without the `sort` parameter, the order of results is unspecified.\",\"example\":\"pet,-age\",\"required\":false},\"limitQueryParam\":{\"in\":\"query\",\"name\":\"limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Return at most this number of rows. A value of 0 is equivalent to having no limit.\",\"example\":\"5\",\"required\":false},\"sortHeaderParam\":{\"in\":\"header\",\"name\":\"X-Sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Same as `sort` query parameter.\",\"example\":\"pet,-age\",\"required\":false},\"limitHeaderParam\":{\"in\":\"header\",\"name\":\"X-Limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Same as `limit` query parameter.\",\"example\":\"5\",\"required\":false},\"colIdPathParam\":{\"in\":\"path\",\"name\":\"colId\",\"schema\":{\"type\":\"string\"},\"description\":\"The column id (without the starting `$`) as shown in the column configuration below the label\",\"required\":true},\"tableIdPathParam\":{\"in\":\"path\",\"name\":\"tableId\",\"schema\":{\"type\":\"string\"},\"description\":\"normalized table name (see `TABLE ID` in Raw Data) or numeric row ID in `_grist_Tables`\",\"required\":true},\"docIdPathParam\":{\"in\":\"path\",\"name\":\"docId\",\"schema\":{\"type\":\"string\"},\"description\":\"A string id (UUID)\",\"required\":true},\"orgIdPathParam\":{\"in\":\"path\",\"name\":\"orgId\",\"schema\":{\"oneOf\":[{\"type\":\"integer\"},{\"type\":\"string\"}]},\"description\":\"This can be an integer id, or a string subdomain (e.g. `gristlabs`), or `current` if the org is implied by the domain in the url\",\"required\":true},\"workspaceIdPathParam\":{\"in\":\"path\",\"name\":\"workspaceId\",\"description\":\"An integer id\",\"schema\":{\"type\":\"integer\"},\"required\":true},\"noparseQueryParam\":{\"in\":\"query\",\"name\":\"noparse\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to prohibit parsing strings according to the column type.\"},\"hiddenQueryParam\":{\"in\":\"query\",\"name\":\"hidden\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to include the hidden columns (like \\\"manualSort\\\")\"},\"headerQueryParam\":{\"in\":\"query\",\"name\":\"header\",\"schema\":{\"type\":\"string\",\"enum\":[\"colId\",\"label\"]},\"description\":\"Format for headers. Labels tend to be more human-friendly while colIds are more normalized.\",\"required\":false}}}}},\"searchIndex\":{\"store\":[\"section/Authentication\",\"tag/orgs\",\"tag/orgs/operation/listOrgs\",\"tag/orgs/operation/describeOrg\",\"tag/orgs/operation/modifyOrg\",\"tag/orgs/operation/listOrgAccess\",\"tag/orgs/operation/modifyOrgAccess\",\"tag/workspaces\",\"tag/workspaces/operation/listWorkspaces\",\"tag/workspaces/operation/createWorkspace\",\"tag/workspaces/operation/describeWorkspace\",\"tag/workspaces/operation/modifyWorkspace\",\"tag/workspaces/operation/deleteWorkspace\",\"tag/workspaces/operation/listWorkspaceAccess\",\"tag/workspaces/operation/modifyWorkspaceAccess\",\"tag/docs\",\"tag/docs/operation/createDoc\",\"tag/docs/operation/describeDoc\",\"tag/docs/operation/modifyDoc\",\"tag/docs/operation/deleteDoc\",\"tag/docs/operation/moveDoc\",\"tag/docs/operation/listDocAccess\",\"tag/docs/operation/modifyDocAccess\",\"tag/docs/operation/downloadDoc\",\"tag/docs/operation/downloadDocXlsx\",\"tag/docs/operation/downloadDocCsv\",\"tag/docs/operation/downloadTableSchema\",\"tag/records\",\"tag/records/operation/listRecords\",\"tag/records/operation/addRecords\",\"tag/records/operation/modifyRecords\",\"tag/records/operation/replaceRecords\",\"tag/tables\",\"tag/tables/operation/listTables\",\"tag/tables/operation/addTables\",\"tag/tables/operation/modifyTables\",\"tag/columns\",\"tag/columns/operation/listColumns\",\"tag/columns/operation/addColumns\",\"tag/columns/operation/modifyColumns\",\"tag/columns/operation/replaceColumns\",\"tag/columns/operation/deleteColumn\",\"tag/data\",\"tag/data/operation/getTableData\",\"tag/data/operation/addRows\",\"tag/data/operation/modifyRows\",\"tag/data/operation/deleteRows\",\"tag/attachments\",\"tag/attachments/operation/listAttachments\",\"tag/attachments/operation/uploadAttachments\",\"tag/attachments/operation/getAttachmentMetadata\",\"tag/attachments/operation/downloadAttachment\",\"tag/webhooks\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/get\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/post\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/patch\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/delete\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1queue/delete\",\"tag/sql\",\"tag/sql/paths/~1docs~1{docId}~1sql/get\",\"tag/sql/paths/~1docs~1{docId}~1sql/post\"],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"title\",\"description\"],\"fieldVectors\":[[\"title/0\",[0,5.068]],[\"description/0\",[1,4.208,2,4.208]],[\"title/1\",[3,2.418]],[\"description/1\",[3,1.097,4,1.984,5,1.776,6,1.984,7,2.3,8,1.621,9,2.3]],[\"title/2\",[3,1.739,10,1.946,11,2.068]],[\"description/2\",[3,1.097,4,1.984,5,1.776,6,1.984,12,2.3,13,2.3,14,2.3]],[\"title/3\",[3,2.023,15,3.275]],[\"description/3\",[16,4.353]],[\"title/4\",[3,2.023,17,2.264]],[\"description/4\",[16,4.353]],[\"title/5\",[3,1.525,10,1.706,11,1.814,18,2.468]],[\"description/5\",[19,4.353]],[\"title/6\",[3,1.739,11,2.068,20,2.569]],[\"description/6\",[19,4.353]],[\"title/7\",[21,2.294]],[\"description/7\",[5,1.953,8,1.782,21,1.145,22,2.529,23,2.529,24,0.689]],[\"title/8\",[3,1.358,10,1.519,21,1.288,24,0.775,25,2.845]],[\"description/8\",[26,4.353]],[\"title/9\",[21,1.65,27,2.815,28,2.815]],[\"description/9\",[26,4.353]],[\"title/10\",[15,3.275,21,1.919]],[\"description/10\",[29,3.896]],[\"title/11\",[17,2.264,21,1.919]],[\"description/11\",[29,3.896]],[\"title/12\",[21,1.919,30,2.988]],[\"description/12\",[29,3.896]],[\"title/13\",[10,1.706,11,1.814,18,2.468,21,1.447]],[\"description/13\",[31,4.353]],[\"title/14\",[11,2.068,20,2.569,21,1.65]],[\"description/14\",[31,4.353]],[\"title/15\",[32,3.914]],[\"description/15\",[21,1.272,24,0.765,33,2.424,34,1.98,35,2.809]],[\"title/16\",[24,0.993,27,2.815,28,2.815]],[\"description/16\",[36,5.045]],[\"title/17\",[15,3.275,24,1.155]],[\"description/17\",[37,3.896]],[\"title/18\",[17,1.706,24,0.87,38,2.468,39,2.08]],[\"description/18\",[37,3.896]],[\"title/19\",[24,1.155,30,2.988]],[\"description/19\",[37,3.896]],[\"title/20\",[3,1.223,21,1.161,24,0.698,40,2.564,41,2.564,42,2.564]],[\"description/20\",[43,5.045]],[\"title/21\",[10,1.706,11,1.814,18,2.468,24,0.87]],[\"description/21\",[44,4.353]],[\"title/22\",[11,2.068,20,2.569,24,0.993]],[\"description/22\",[44,4.353]],[\"title/23\",[24,0.87,39,2.08,45,3.196,46,2.252]],[\"description/23\",[47,5.045]],[\"title/24\",[24,0.87,39,2.08,46,2.252,48,3.196]],[\"description/24\",[49,5.045]],[\"title/25\",[39,2.08,46,2.252,50,0.833,51,3.196]],[\"description/25\",[52,5.045]],[\"title/26\",[50,1.105,53,3.658]],[\"description/26\",[53,2.182,54,2.529,55,2.529,56,2.529,57,2.529,58,2.529]],[\"title/27\",[59,2.294]],[\"description/27\",[8,1.782,33,2.182,34,1.782,50,0.659,59,1.145,60,1.782]],[\"title/28\",[50,0.95,59,1.65,61,3.144]],[\"description/28\",[62,3.556]],[\"title/29\",[50,0.95,59,1.65,63,2.209]],[\"description/29\",[62,3.556]],[\"title/30\",[17,1.946,50,0.95,59,1.65]],[\"description/30\",[62,3.556]],[\"title/31\",[50,0.833,59,1.447,63,1.937,64,2.757]],[\"description/31\",[62,3.556]],[\"title/32\",[50,1.321]],[\"description/32\",[24,0.86,34,2.227,50,0.823,65,2.726]],[\"title/33\",[10,1.946,24,0.993,50,0.95]],[\"description/33\",[66,3.896]],[\"title/34\",[24,0.993,50,0.95,63,2.209]],[\"description/34\",[66,3.896]],[\"title/35\",[17,1.946,24,0.993,50,0.95]],[\"description/35\",[66,3.896]],[\"title/36\",[67,2.706]],[\"description/36\",[34,2.227,50,0.823,65,2.726,67,1.687]],[\"title/37\",[10,1.946,50,0.95,67,1.946]],[\"description/37\",[68,3.556]],[\"title/38\",[50,0.95,63,2.209,67,1.946]],[\"description/38\",[68,3.556]],[\"title/39\",[17,1.946,50,0.95,67,1.946]],[\"description/39\",[68,3.556]],[\"title/40\",[50,0.833,63,1.937,64,2.757,67,1.706]],[\"description/40\",[68,3.556]],[\"title/41\",[30,2.569,50,0.95,67,1.946]],[\"description/41\",[69,5.045]],[\"title/42\",[70,3.572]],[\"description/42\",[50,0.412,59,0.716,70,1.115,71,1.582,72,1.365,73,2.652,74,1.115,75,1.582,76,1.582,77,1.582,78,1.115]],[\"title/43\",[50,0.95,61,3.144,70,2.569]],[\"description/43\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/44\",[50,0.95,60,2.569,63,2.209]],[\"description/44\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/45\",[17,1.946,50,0.95,60,2.569]],[\"description/45\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/46\",[30,2.569,50,0.95,60,2.569]],[\"description/46\",[90,5.045]],[\"title/47\",[91,3.071]],[\"description/47\",[24,0.46,46,1.189,59,0.764,67,0.901,70,1.189,72,1.456,91,1.696,92,1.687,93,1.687,94,1.687]],[\"title/48\",[10,1.706,32,2.468,38,2.468,91,1.937]],[\"description/48\",[95,4.353]],[\"title/49\",[32,2.815,91,2.209,96,3.645]],[\"description/49\",[95,4.353]],[\"title/50\",[38,3.275,91,2.569]],[\"description/50\",[97,5.045]],[\"title/51\",[39,2.372,91,2.209,98,3.645]],[\"description/51\",[99,5.045]],[\"title/52\",[100,3.071]],[\"description/52\",[8,1.621,20,1.621,24,0.626,100,1.394,101,2.3,102,2.3,103,2.3]],[\"title/53\",[24,0.993,100,2.209,104,3.645]],[\"description/53\",[105,4.353]],[\"title/54\",[24,0.87,27,2.468,87,2.252,100,1.937]],[\"description/54\",[105,4.353]],[\"title/55\",[17,2.264,100,2.569]],[\"description/55\",[106,4.353]],[\"title/56\",[82,2.988,100,2.569]],[\"description/56\",[106,4.353]],[\"title/57\",[28,2.198,107,2.845,108,2.845,109,2.845,110,2.845]],[\"description/57\",[111,5.045]],[\"title/58\",[112,3.914]],[\"description/58\",[]],[\"title/59\",[24,0.775,112,2.198,113,2.455,114,2.455,115,2.455]],[\"description/59\",[116,4.353]],[\"title/60\",[24,0.636,112,1.802,113,2.013,114,2.013,115,2.013,117,2.334,118,2.334]],[\"description/60\",[116,4.353]]],\"invertedIndex\":[[\"\",{\"_index\":2,\"title\":{},\"description\":{\"0\":{}}}],[\"access\",{\"_index\":11,\"title\":{\"2\":{},\"5\":{},\"6\":{},\"13\":{},\"14\":{},\"21\":{},\"22\":{}},\"description\":{}}],[\"add\",{\"_index\":63,\"title\":{\"29\":{},\"31\":{},\"34\":{},\"38\":{},\"40\":{},\"44\":{}},\"description\":{}}],[\"against\",{\"_index\":115,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"anoth\",{\"_index\":41,\"title\":{\"20\":{}},\"description\":{}}],[\"api\",{\"_index\":9,\"title\":{},\"description\":{\"1\":{}}}],[\"area\",{\"_index\":13,\"title\":{},\"description\":{\"2\":{}}}],[\"associ\",{\"_index\":104,\"title\":{\"53\":{}},\"description\":{}}],[\"attach\",{\"_index\":91,\"title\":{\"47\":{},\"48\":{},\"49\":{},\"50\":{},\"51\":{}},\"description\":{\"47\":{}}}],[\"authent\",{\"_index\":0,\"title\":{\"0\":{}},\"description\":{}}],[\"avail\",{\"_index\":14,\"title\":{},\"description\":{\"2\":{}}}],[\"better\",{\"_index\":84,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"call\",{\"_index\":8,\"title\":{},\"description\":{\"1\":{},\"7\":{},\"27\":{},\"52\":{}}}],[\"chang\",{\"_index\":20,\"title\":{\"6\":{},\"14\":{},\"22\":{}},\"description\":{\"52\":{}}}],[\"collect\",{\"_index\":34,\"title\":{},\"description\":{\"15\":{},\"27\":{},\"32\":{},\"36\":{}}}],[\"column\",{\"_index\":67,\"title\":{\"36\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{}},\"description\":{\"36\":{},\"47\":{}}}],[\"columnar\",{\"_index\":75,\"title\":{},\"description\":{\"42\":{}}}],[\"consid\",{\"_index\":83,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"contain\",{\"_index\":33,\"title\":{},\"description\":{\"15\":{},\"27\":{}}}],[\"content\",{\"_index\":39,\"title\":{\"18\":{},\"23\":{},\"24\":{},\"25\":{},\"51\":{}},\"description\":{}}],[\"creat\",{\"_index\":27,\"title\":{\"9\":{},\"16\":{},\"54\":{}},\"description\":{}}],[\"csv\",{\"_index\":51,\"title\":{\"25\":{}},\"description\":{}}],[\"data\",{\"_index\":70,\"title\":{\"42\":{},\"43\":{}},\"description\":{\"42\":{},\"47\":{}}}],[\"delet\",{\"_index\":30,\"title\":{\"12\":{},\"19\":{},\"41\":{},\"46\":{}},\"description\":{}}],[\"deprec\",{\"_index\":74,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"describ\",{\"_index\":15,\"title\":{\"3\":{},\"10\":{},\"17\":{}},\"description\":{}}],[\"doc\",{\"_index\":32,\"title\":{\"15\":{},\"48\":{},\"49\":{}},\"description\":{}}],[\"docs/{docid\",{\"_index\":37,\"title\":{},\"description\":{\"17\":{},\"18\":{},\"19\":{}}}],[\"docs/{docid}/access\",{\"_index\":44,\"title\":{},\"description\":{\"21\":{},\"22\":{}}}],[\"docs/{docid}/attach\",{\"_index\":95,\"title\":{},\"description\":{\"48\":{},\"49\":{}}}],[\"docs/{docid}/attachments/{attachmentid\",{\"_index\":97,\"title\":{},\"description\":{\"50\":{}}}],[\"docs/{docid}/attachments/{attachmentid}/download\",{\"_index\":99,\"title\":{},\"description\":{\"51\":{}}}],[\"docs/{docid}/download\",{\"_index\":47,\"title\":{},\"description\":{\"23\":{}}}],[\"docs/{docid}/download/csv\",{\"_index\":52,\"title\":{},\"description\":{\"25\":{}}}],[\"docs/{docid}/download/table-schema\",{\"_index\":58,\"title\":{},\"description\":{\"26\":{}}}],[\"docs/{docid}/download/xlsx\",{\"_index\":49,\"title\":{},\"description\":{\"24\":{}}}],[\"docs/{docid}/mov\",{\"_index\":43,\"title\":{},\"description\":{\"20\":{}}}],[\"docs/{docid}/sql\",{\"_index\":116,\"title\":{},\"description\":{\"59\":{},\"60\":{}}}],[\"docs/{docid}/t\",{\"_index\":66,\"title\":{},\"description\":{\"33\":{},\"34\":{},\"35\":{}}}],[\"docs/{docid}/tables/{tableid}/column\",{\"_index\":68,\"title\":{},\"description\":{\"37\":{},\"38\":{},\"39\":{},\"40\":{}}}],[\"docs/{docid}/tables/{tableid}/columns/{colid\",{\"_index\":69,\"title\":{},\"description\":{\"41\":{}}}],[\"docs/{docid}/tables/{tableid}/data\",{\"_index\":89,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"docs/{docid}/tables/{tableid}/data/delet\",{\"_index\":90,\"title\":{},\"description\":{\"46\":{}}}],[\"docs/{docid}/tables/{tableid}/record\",{\"_index\":62,\"title\":{},\"description\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{}}}],[\"docs/{docid}/webhook\",{\"_index\":105,\"title\":{},\"description\":{\"53\":{},\"54\":{}}}],[\"docs/{docid}/webhooks/queu\",{\"_index\":111,\"title\":{},\"description\":{\"57\":{}}}],[\"docs/{docid}/webhooks/{webhookid\",{\"_index\":106,\"title\":{},\"description\":{\"55\":{},\"56\":{}}}],[\"document\",{\"_index\":24,\"title\":{\"8\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"33\":{},\"34\":{},\"35\":{},\"53\":{},\"54\":{},\"59\":{},\"60\":{}},\"description\":{\"7\":{},\"15\":{},\"32\":{},\"47\":{},\"52\":{}}}],[\"document'\",{\"_index\":107,\"title\":{\"57\":{}},\"description\":{}}],[\"download\",{\"_index\":98,\"title\":{\"51\":{}},\"description\":{}}],[\"empti\",{\"_index\":28,\"title\":{\"9\":{},\"16\":{},\"57\":{}},\"description\":{}}],[\"endpoint\",{\"_index\":78,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"enumer\",{\"_index\":12,\"title\":{},\"description\":{\"2\":{}}}],[\"excel\",{\"_index\":48,\"title\":{\"24\":{}},\"description\":{}}],[\"favor\",{\"_index\":79,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"fetch\",{\"_index\":61,\"title\":{\"28\":{},\"43\":{}},\"description\":{}}],[\"file\",{\"_index\":46,\"title\":{\"23\":{},\"24\":{},\"25\":{}},\"description\":{\"47\":{}}}],[\"follow\",{\"_index\":54,\"title\":{},\"description\":{\"26\":{}}}],[\"format\",{\"_index\":76,\"title\":{},\"description\":{\"42\":{}}}],[\"frictionlessdata'\",{\"_index\":55,\"title\":{},\"description\":{\"26\":{}}}],[\"grist\",{\"_index\":35,\"title\":{},\"description\":{\"15\":{}}}],[\"group\",{\"_index\":23,\"title\":{},\"description\":{\"7\":{}}}],[\"immedi\",{\"_index\":80,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"includ\",{\"_index\":92,\"title\":{},\"description\":{\"47\":{}}}],[\"list\",{\"_index\":10,\"title\":{\"2\":{},\"5\":{},\"8\":{},\"13\":{},\"21\":{},\"33\":{},\"37\":{},\"48\":{}},\"description\":{}}],[\"metadata\",{\"_index\":38,\"title\":{\"18\":{},\"48\":{},\"50\":{}},\"description\":{}}],[\"modifi\",{\"_index\":17,\"title\":{\"4\":{},\"11\":{},\"18\":{},\"30\":{},\"35\":{},\"39\":{},\"45\":{},\"55\":{}},\"description\":{}}],[\"move\",{\"_index\":40,\"title\":{\"20\":{}},\"description\":{}}],[\"new\",{\"_index\":87,\"title\":{\"54\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"now\",{\"_index\":73,\"title\":{},\"description\":{\"42\":{}}}],[\"option\",{\"_index\":117,\"title\":{\"60\":{}},\"description\":{}}],[\"org\",{\"_index\":3,\"title\":{\"1\":{},\"2\":{},\"3\":{},\"4\":{},\"5\":{},\"6\":{},\"8\":{},\"20\":{}},\"description\":{\"1\":{},\"2\":{}}}],[\"organ\",{\"_index\":22,\"title\":{},\"description\":{\"7\":{}}}],[\"orgs/{orgid\",{\"_index\":16,\"title\":{},\"description\":{\"3\":{},\"4\":{}}}],[\"orgs/{orgid}/access\",{\"_index\":19,\"title\":{},\"description\":{\"5\":{},\"6\":{}}}],[\"orgs/{orgid}/workspac\",{\"_index\":26,\"title\":{},\"description\":{\"8\":{},\"9\":{}}}],[\"paramet\",{\"_index\":118,\"title\":{\"60\":{}},\"description\":{}}],[\"payload\",{\"_index\":110,\"title\":{\"57\":{}},\"description\":{}}],[\"person\",{\"_index\":6,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"plan\",{\"_index\":81,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"point\",{\"_index\":86,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"project\",{\"_index\":88,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"queri\",{\"_index\":114,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"queue\",{\"_index\":108,\"title\":{\"57\":{}},\"description\":{}}],[\"recommend\",{\"_index\":77,\"title\":{},\"description\":{\"42\":{}}}],[\"record\",{\"_index\":59,\"title\":{\"27\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{}},\"description\":{\"27\":{},\"42\":{},\"43\":{},\"44\":{},\"45\":{},\"47\":{}}}],[\"refer\",{\"_index\":93,\"title\":{},\"description\":{\"47\":{}}}],[\"remov\",{\"_index\":82,\"title\":{\"56\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"request\",{\"_index\":102,\"title\":{},\"description\":{\"52\":{}}}],[\"row\",{\"_index\":60,\"title\":{\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{}}}],[\"run\",{\"_index\":113,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"same\",{\"_index\":42,\"title\":{\"20\":{}},\"description\":{}}],[\"schema\",{\"_index\":53,\"title\":{\"26\":{}},\"description\":{\"26\":{}}}],[\"securitydefinit\",{\"_index\":1,\"title\":{},\"description\":{\"0\":{}}}],[\"site\",{\"_index\":5,\"title\":{},\"description\":{\"1\":{},\"2\":{},\"7\":{}}}],[\"space\",{\"_index\":7,\"title\":{},\"description\":{\"1\":{}}}],[\"sql\",{\"_index\":112,\"title\":{\"58\":{},\"59\":{},\"60\":{}},\"description\":{}}],[\"sqlite\",{\"_index\":45,\"title\":{\"23\":{}},\"description\":{}}],[\"standard](https://specs.frictionlessdata.io/table-schema\",{\"_index\":57,\"title\":{},\"description\":{\"26\":{}}}],[\"start\",{\"_index\":85,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"structur\",{\"_index\":65,\"title\":{},\"description\":{\"32\":{},\"36\":{}}}],[\"tabl\",{\"_index\":50,\"title\":{\"25\":{},\"26\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{},\"43\":{},\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{},\"32\":{},\"36\":{},\"42\":{}}}],[\"table-schema\",{\"_index\":56,\"title\":{},\"description\":{\"26\":{}}}],[\"team\",{\"_index\":4,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"trigger\",{\"_index\":101,\"title\":{},\"description\":{\"52\":{}}}],[\"type\",{\"_index\":94,\"title\":{},\"description\":{\"47\":{}}}],[\"undeliv\",{\"_index\":109,\"title\":{\"57\":{}},\"description\":{}}],[\"updat\",{\"_index\":64,\"title\":{\"31\":{},\"40\":{}},\"description\":{}}],[\"upload\",{\"_index\":96,\"title\":{\"49\":{}},\"description\":{}}],[\"url\",{\"_index\":103,\"title\":{},\"description\":{\"52\":{}}}],[\"us\",{\"_index\":72,\"title\":{},\"description\":{\"42\":{},\"47\":{}}}],[\"user\",{\"_index\":18,\"title\":{\"5\":{},\"13\":{},\"21\":{}},\"description\":{}}],[\"webhook\",{\"_index\":100,\"title\":{\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{}},\"description\":{\"52\":{}}}],[\"within\",{\"_index\":25,\"title\":{\"8\":{}},\"description\":{}}],[\"work\",{\"_index\":71,\"title\":{},\"description\":{\"42\":{}}}],[\"workspac\",{\"_index\":21,\"title\":{\"7\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{},\"12\":{},\"13\":{},\"14\":{},\"20\":{}},\"description\":{\"7\":{},\"15\":{}}}],[\"workspaces/{workspaceid\",{\"_index\":29,\"title\":{},\"description\":{\"10\":{},\"11\":{},\"12\":{}}}],[\"workspaces/{workspaceid}/access\",{\"_index\":31,\"title\":{},\"description\":{\"13\":{},\"14\":{}}}],[\"workspaces/{workspaceid}/doc\",{\"_index\":36,\"title\":{},\"description\":{\"16\":{}}}]],\"pipeline\":[]}},\"options\":{\"theme\":{\"spacing\":{\"sectionVertical\":2},\"breakpoints\":{\"medium\":\"50rem\",\"large\":\"50rem\"},\"sidebar\":{\"width\":\"0px\"}},\"hideDownloadButton\":true,\"pathInMiddlePanel\":true,\"scrollYOffset\":48,\"jsonSampleExpandLevel\":\"all\"}}; var container = document.getElementById('redoc'); Redoc.hydrate(__redoc_state, container);","title":"REST API reference"},{"location":"api/#grist-api-reference","text":"REST API for manipulating documents, workspaces, and team sites. API Usage is an introduction to using the API. API Console allows you to make API calls from the browser. Authentication orgs get List the orgs you have access to get Describe an org patch Modify an org get List users with access to org patch Change who has access to org workspaces get List workspaces and documents within an org post Create an empty workspace get Describe a workspace patch Modify a workspace del Delete a workspace get List users with access to workspace patch Change who has access to workspace docs post Create an empty document get Describe a document patch Modify document metadata (but not its contents) del Delete a document patch Move document to another workspace in the same org. get List users with access to document patch Change who has access to document get Content of document, as an Sqlite file get Content of document, as an Excel file get Content of table, as a CSV file get The schema of a table records get Fetch records from a table post Add records to a table patch Modify records of a table put Add or update records of a table tables get List tables in a document post Add tables to a document patch Modify tables of a document columns get List columns in a table post Add columns to a table patch Modify columns of a table put Add or update columns of a table del Delete a column of a table data get Fetch data from a table post Add rows to a table patch Modify rows of a table post Delete rows of a table attachments get List metadata of all attachments in a doc post Upload attachments to a doc get Get the metadata for an attachment get Download the contents of an attachment webhooks get Webhooks associated with a document post Create new webhooks for a document patch Modify a webhook del Remove a webhook del Empty a document's queue of undelivered payloads sql get Run an SQL query against a document post Run an SQL query against a document, with options or parameters API docs by Redocly","title":"Grist API Reference"},{"location":"integrators/","text":"Integrator Services # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make Configuring Integrators # Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables. Example: Storing form submissions # Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in! Example: Sending email alerts # We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win! Readiness column # Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example . Triggering (or avoiding triggering) on pre-existing records # The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Integrator Services"},{"location":"integrators/#integrator-services","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make","title":"Integrator Services"},{"location":"integrators/#configuring-integrators","text":"Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables.","title":"Configuring Integrators"},{"location":"integrators/#example-storing-form-submissions","text":"Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in!","title":"Example: Storing form submissions"},{"location":"integrators/#example-sending-email-alerts","text":"We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win!","title":"Example: Sending email alerts"},{"location":"integrators/#readiness-column","text":"Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example .","title":"Readiness column"},{"location":"integrators/#triggering-or-avoiding-triggering-on-pre-existing-records","text":"The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Triggering (or avoiding triggering) on pre-existing records"},{"location":"embedding/","text":"Embedding Grist # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view: Parameters # Read-Only vs. Editable # Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing . Appearance # Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Embedding"},{"location":"embedding/#embedding-grist","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view:","title":"Embedding Grist"},{"location":"embedding/#parameters","text":"","title":"Parameters"},{"location":"embedding/#read-only-vs-editable","text":"Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing .","title":"Read-Only vs. Editable"},{"location":"embedding/#appearance","text":"Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Appearance"},{"location":"code/","text":"Plugin API # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Intro to Plugin API"},{"location":"code/#plugin-api","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Plugin API"},{"location":"code/modules/grist_plugin_api/","text":"Module: grist-plugin-api # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Table of contents # Interfaces # AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap Type Aliases # ColumnsToMap UIRowId Variables # checkers docApi sectionApi selectedTable viewApi widgetApi Functions # allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows Type Aliases # ColumnsToMap # \u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget. UIRowId # \u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row. Variables # checkers # \u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above. docApi # \u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default. sectionApi # \u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget. selectedTable # \u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets). viewApi # \u2022 Const viewApi : GristView Interface for the records backing a custom widget. widgetApi # \u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget. Functions # allowSelectBy # \u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message. Returns # Promise < void > clearOptions # \u25b8 clearOptions (): Promise < void > Clears all the options. Returns # Promise < void > fetchSelectedRecord # \u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default. Parameters # Name Type rowId number options FetchSelectedOptions Returns # Promise < any > fetchSelectedTable # \u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default. Parameters # Name Type options FetchSelectedOptions Returns # Promise < any > getAccessToken # \u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); }); Parameters # Name Type options? AccessTokenOptions Returns # Promise < AccessTokenResult > getOption # \u25b8 getOption ( key ): Promise < any > Get single value from Widget options object. Parameters # Name Type key string Returns # Promise < any > getOptions # \u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object. Returns # Promise < null | object > getTable # \u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted. Parameters # Name Type tableId? string Returns # TableOperations mapColumnNames # \u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean Returns # any mapColumnNamesBack # \u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap Returns # any on # \u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101 Parameters # Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function Returns # Rpc onNewRecord # \u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected. Parameters # Name Type callback ( mappings : null | WidgetColumnMap ) => unknown Returns # void onOptions # \u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it. Parameters # Name Type callback ( options : any , settings : InteractionOptions ) => unknown Returns # void onRecord # \u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false . Parameters # Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void onRecords # \u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false . Parameters # Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void ready # \u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called. Parameters # Name Type settings? ReadyPayload Returns # void setCursorPos # \u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking. Parameters # Name Type pos CursorPos Returns # Promise < void > setOption # \u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary). Parameters # Name Type key string value any Returns # Promise < void > setOptions # \u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget. Parameters # Name Type options Object Returns # Promise < void > setSelectedRows # \u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget. Parameters # Name Type rowIds null | number [] Returns # Promise < void >","title":"grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#module-grist-plugin-api","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing .","title":"Module: grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/grist_plugin_api/#interfaces","text":"AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap","title":"Interfaces"},{"location":"code/modules/grist_plugin_api/#type-aliases","text":"ColumnsToMap UIRowId","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#variables","text":"checkers docApi sectionApi selectedTable viewApi widgetApi","title":"Variables"},{"location":"code/modules/grist_plugin_api/#functions","text":"allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows","title":"Functions"},{"location":"code/modules/grist_plugin_api/#type-aliases_1","text":"","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#columnstomap","text":"\u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget.","title":"ColumnsToMap"},{"location":"code/modules/grist_plugin_api/#uirowid","text":"\u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row.","title":"UIRowId"},{"location":"code/modules/grist_plugin_api/#variables_1","text":"","title":"Variables"},{"location":"code/modules/grist_plugin_api/#checkers","text":"\u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above.","title":"checkers"},{"location":"code/modules/grist_plugin_api/#docapi","text":"\u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default.","title":"docApi"},{"location":"code/modules/grist_plugin_api/#sectionapi","text":"\u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget.","title":"sectionApi"},{"location":"code/modules/grist_plugin_api/#selectedtable","text":"\u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets).","title":"selectedTable"},{"location":"code/modules/grist_plugin_api/#viewapi","text":"\u2022 Const viewApi : GristView Interface for the records backing a custom widget.","title":"viewApi"},{"location":"code/modules/grist_plugin_api/#widgetapi","text":"\u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget.","title":"widgetApi"},{"location":"code/modules/grist_plugin_api/#functions_1","text":"","title":"Functions"},{"location":"code/modules/grist_plugin_api/#allowselectby","text":"\u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message.","title":"allowSelectBy"},{"location":"code/modules/grist_plugin_api/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#clearoptions","text":"\u25b8 clearOptions (): Promise < void > Clears all the options.","title":"clearOptions"},{"location":"code/modules/grist_plugin_api/#returns_1","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedrecord","text":"\u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default.","title":"fetchSelectedRecord"},{"location":"code/modules/grist_plugin_api/#parameters","text":"Name Type rowId number options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_2","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedtable","text":"\u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default.","title":"fetchSelectedTable"},{"location":"code/modules/grist_plugin_api/#parameters_1","text":"Name Type options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_3","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getaccesstoken","text":"\u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); });","title":"getAccessToken"},{"location":"code/modules/grist_plugin_api/#parameters_2","text":"Name Type options? AccessTokenOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_4","text":"Promise < AccessTokenResult >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoption","text":"\u25b8 getOption ( key ): Promise < any > Get single value from Widget options object.","title":"getOption"},{"location":"code/modules/grist_plugin_api/#parameters_3","text":"Name Type key string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_5","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoptions","text":"\u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object.","title":"getOptions"},{"location":"code/modules/grist_plugin_api/#returns_6","text":"Promise < null | object >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#gettable","text":"\u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted.","title":"getTable"},{"location":"code/modules/grist_plugin_api/#parameters_4","text":"Name Type tableId? string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_7","text":"TableOperations","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnames","text":"\u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping.","title":"mapColumnNames"},{"location":"code/modules/grist_plugin_api/#parameters_5","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_8","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnamesback","text":"\u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically.","title":"mapColumnNamesBack"},{"location":"code/modules/grist_plugin_api/#parameters_6","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_9","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#on","text":"\u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101","title":"on"},{"location":"code/modules/grist_plugin_api/#parameters_7","text":"Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_10","text":"Rpc","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onnewrecord","text":"\u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected.","title":"onNewRecord"},{"location":"code/modules/grist_plugin_api/#parameters_8","text":"Name Type callback ( mappings : null | WidgetColumnMap ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_11","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onoptions","text":"\u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it.","title":"onOptions"},{"location":"code/modules/grist_plugin_api/#parameters_9","text":"Name Type callback ( options : any , settings : InteractionOptions ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_12","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecord","text":"\u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false .","title":"onRecord"},{"location":"code/modules/grist_plugin_api/#parameters_10","text":"Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_13","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecords","text":"\u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false .","title":"onRecords"},{"location":"code/modules/grist_plugin_api/#parameters_11","text":"Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_14","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#ready","text":"\u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called.","title":"ready"},{"location":"code/modules/grist_plugin_api/#parameters_12","text":"Name Type settings? ReadyPayload","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_15","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setcursorpos","text":"\u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking.","title":"setCursorPos"},{"location":"code/modules/grist_plugin_api/#parameters_13","text":"Name Type pos CursorPos","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_16","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoption","text":"\u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary).","title":"setOption"},{"location":"code/modules/grist_plugin_api/#parameters_14","text":"Name Type key string value any","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_17","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoptions","text":"\u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget.","title":"setOptions"},{"location":"code/modules/grist_plugin_api/#parameters_15","text":"Name Type options Object","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_18","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setselectedrows","text":"\u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget.","title":"setSelectedRows"},{"location":"code/modules/grist_plugin_api/#parameters_16","text":"Name Type rowIds null | number []","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_19","text":"Promise < void >","title":"Returns"},{"location":"self-managed/","text":"Self-Managed Grist # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Self-Managed Grist The essentials What is Self-Managed Grist? How do I install Grist? Grist on AWS How do I sandbox documents? XSAVE not available PTRACE not available How do I run Grist on a server? How do I set up a team? How do I set up authentication? Are there other authentication methods? How do I activate Grist Enterprise? Customization How do I customize styling? How do I list custom widgets? How do I set up email notifications? How do I add more python packages? Operations What are the hardware requirements for hosting Grist? What files does Grist store? What is a \u201chome\u201d database? What is a state store? How do I set up snapshots? How do I control telemetry? How do I upgrade my installation? What if I need high availability? The essentials # What is Self-Managed Grist? # There are four flavors of Grist: SaaS (Software as a Service): Grist is available as a hosted service at docs.getgrist.com . No installation needed. Free and paid plans, with usage limits. Desktop App : Grist is available as a desktop application, built with Electron. It is available for download at https://github.com/gristlabs/grist-electron/releases . This desktop application does not need internet and is not tied to any online account or service. Self-Managed Enterprise : Grist is available as a licensed application installed by enterprises on their own infrastructure with our support and backing. Contains proprietary features developed for enterprises with particular needs. Self-Managed Core : Grist is available as a free application installed by citizen developers on their own infrastructure with community support. Grist documents created with our SaaS and Enterprise offerings can be opened and edited with Core, and vice versa. This establishes Grist documents as a reliable format for archiving and interchange. Self-Managed Grist, be it Enterprise or Core, is installed and configured in much the same way, as described in the following sections. For clarity, the sections are tagged with which flavor they apply to, for example: The full source code for Grist Core is always available at github.com/gristlabs/grist-core and is under an Apache-2.0 license. You may use and redistribute Core freely, under the terms of the free software license. The full source for Grist Enterprise is also available, at github.com/gristlabs/grist-ee , under a proprietary license that does not grant any automatic rights to use or redistribute the software. You can evaluate Enterprise for 30 days using the instructions in the following sections, or sign up for our Grist Enterprise plan and get support. How do I install Grist? # The easiest way to install Grist is as a container. We will describe how using Docker , but there are many other tools and services for running containers. To try Grist out using Docker, make an empty directory for Grist to store material in (say ~/grist ) and then for Grist Core you can do: docker run -p 8484:8484 \\ -v ~/grist:/persist \\ -e GRIST_SESSION_SECRET=invent-a-secret-here \\ -it gristlabs/grist For Grist Enterprise use gristlabs/grist-ee instead of gristlabs/grist . You should then be able to visit http://localhost:8484 in your browser. Already you will be able to create and edit Grist documents, and to open and edit documents downloaded from another Grist installation (such as our SaaS). If using some other tool or service, here are the important points: The container name is gristlabs/grist or gristlabs/grist-ee (for some tools, you may need to prefix these names with docker.io/ ). A volume (or mount, or directory) needs to be available at location /persist within the container. It can be initially empty - Grist will populate it. Without this volume, nothing you do will be stored long-term. Port 8484 on the container needs to be exposed. This can be changed if you also set the PORT environment variable for the container. The environment variable GRIST_SESSION_SECRET should be set to something secret for the container. Installed this way, Grist is accessible only to you. Typically you want to take at least the following steps: Set up sandboxing - this is important to place bounds on what formulas can do. Serve from a public host so you can collaborate live with others. Enable an authentication method so users can log in. Often you\u2019ll want to hook Grist up to an \u201cSSO\u201d (Single Sign-On) service you already use. We support some very general authentication methods that cover many cases, and a special authentication method for custom cases. Consider enabling snapshot support if you want Grist to handle document backups. Grist on AWS # You can also host Grist on AWS. Full instructions on this hosting method are available on the Grist AWS Marketplace page . How do I sandbox documents? # Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network. docker run ... -e GRIST_SANDBOX_FLAVOR=gvisor \\ ... To sanity-check that formulas are being evaluated within a sandbox, you can create a document and then check that this formula gives an empty result: import glob glob.glob('/etc/*') Here are some reasons why gvisor sandboxing, as configured for Grist, may fail, and what you can do to diagnose the problem. XSAVE not available # Your processor may not be supported. On x86_64 , Sandy Bridge or later is needed. Check that the XSAVE processor flag is set. Here\u2019s a quick way to test that: grep -q '\\bxsave\\b' /proc/cpuinfo && echo \"XSAVE enabled\" || echo \"XSAVE missing\" PTRACE not available # The SYS_PTRACE capability may not be available. If running in docker, you could try explicitly granting it, if you are comfortable with making it available: docker run ... --cap-add=SYS_PTRACE ... In some cloud environments such as AWS ECS, you may need to explicitly list this capability in your container configuration. How do I run Grist on a server? # We suggest that you become familiar with all the other aspects of self-management on this page before serving Grist from a public host (especially Sandboxing ). When you do, it is important to tell Grist where it will be served from, using the APP_HOME_URL variable. For example, if you will be serving from https://grist.example.com , let Grist know like this: docker run ... -e APP_HOME_URL=\"https://grist.example.com\" \\ ... You will need to place a \u201creverse proxy\u201d in front of Grist to handle \u201cSSL termination\u201d (decrypting encypted traffic) using a certificate that establishes ownership of the site. If you don\u2019t know what this means, you could try using the Grist Omnibus which packages Grist with a reverse proxy that will use Let\u2019s Encrypt to get a certificate for you automatically. An important job of such a proxy is to correctly forward websocket connections. This amounts to two requirements: Ensure that the proxy is using HTTP 1.1 Pass the necessary Upgrade, Connection, and Host HTTP headers so that an HTTP connection can be upgraded to a websocket connection. For example, here is a minimal configuration for nginx , a possible choice for reverse proxy. server { server_name grist.example.com; location / { proxy_pass http://localhost:8484; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # WebSocket support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } } This configuration will handle basic HTTP traffic and websockets. It still requires additional SSL/TLS configuration. A simple option for self-hosting on a small scale is to use certbot by the EFF . How do I set up a team? # Grist has a concept of \u201cteam sites\u201d that are independently managed and named areas containing their own workspaces and documents. Team sites can have distinct subdomains (as on our SaaS\u2019s hosted team sites ), or be distinguished by a special path prefix. This often does not make sense for self-managed installations, where there is a single team. With a single domain and a single team, the special path prefix (which looks like /o/from other elements using blank lines. Configuring submission options # You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel. Publishing your form # Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error. Form submissions # After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form"},{"location":"widget-form/#page-widget-form","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The form widget allows you to collect data in a form view which populates your Grist data table upon submission.","title":"Page widget: Form"},{"location":"widget-form/#setting-up-your-data","text":"Create a table containing the columns of data you wish to populate via form.","title":"Setting up your data"},{"location":"widget-form/#creating-your-form","text":"Add a form widget from the \u201cAdd New\u201d menu. Select the data table you wish to populate with form data. Then, customize the form to your heart\u2019s desire! By default, the form view will include elements for headers and descriptions as well as all columns (fields) from the underlying data table.","title":"Creating your form"},{"location":"widget-form/#adding-and-removing-elements","text":"To add additional form elements, click the + icon at the bottom of the form. From the menu, you can add the following elements: New Question: Select a column type to create a new field. \u201c\u2022\u2022\u2022 More >\u201d will open an expanded menu listing all column types. Adding a new question will add a new column to the underlying data table. Unmapped Fields: Lists any hidden fields from the underlying data table. Building Blocks: Customize further by adding these additional elements! You can remove any element from the form by hovering over the object and clicking the trash icon to delete. You can hide any uneccessary fields from the form by hovering over the object and clicking the x icon.","title":"Adding and removing elements"},{"location":"widget-form/#configuring-fields","text":"You can provide alternative titles for your form fields, rather than use the same column name from the underlying data table. For example, on our form, we have a toggle that is titled \u201cMay we contact you?\u201d. In the data table, this column is labeled \u201cOk to Contact?\u201d. Field titles can be configured under the \u201cField\u201d tab of the creator panel. To make a form field required, check the box next to \u201cRequired field\u201d. If a user attempts to submit a form without filling in the required field, they will get an alert to fill out the field.","title":"Configuring fields"},{"location":"widget-form/#configuring-building-blocks","text":"Header and Paragraph building blocks can be edited either directly in the block or from the creator panel. In the creator panel, you have text alignment options available. For additional formatting, both elements allow the use of Markdown formatting. For help on Markdown formatting, check out the Markdown Guide . HTML Formatting HTML tags can be used in Markdown-formatted text. Be sure to separate block-level HTML elements like
from other elements using blank lines.","title":"Configuring building blocks"},{"location":"widget-form/#configuring-submission-options","text":"You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel.","title":"Configuring submission options"},{"location":"widget-form/#publishing-your-form","text":"Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error.","title":"Publishing your form"},{"location":"widget-form/#form-submissions","text":"After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form submissions"},{"location":"widget-chart/","text":"Page widget: Chart # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist supports several chart types to help you visualize your data easily. Charts may be used to plot a regular table of data, a linked widget (as described in Linking widgets ), or a summary table (as described in Summary tables ). The most common chart types are illustrated here: Chart types # Each chart type plots one or several data series. Select the series to plot by clicking the green \u2018Add Series\u2019 text in the creator panel . In the area above the \u201cSeries\u201d section of the creator panel, you may configure the x-axis for most charts, or the labels for pie and donut charts. Bar Chart # Needs an x-axis and at least one series to plot along the y-axis. Additional series create additional bars at each point on the x-axis. To stack series onto the same bar, select the \u201cStack series\u201d checkbox. Line Chart # Needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines. Pie Chart # Needs pie slice labels and one series for the pie slice sizes. Area Chart # Similar to a line chart, needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines. Scatter Plot # Needs a label and two or more series. The label applies to the points. The series apply the X and Y values for each point, respectively. Additional series specify Y values for additional sets of points. Kaplan-Meier Plot # The Kaplan-Meier Plot is useful for certain studies, and needs one label and one series. The label applies to the lines being plotted. The series gives a survival time or time-to-failure of that point. The plot shows the survival times on the X axis, and the number of points that survive at that time on the Y axis. Chart options # A number of chart options are available, some of them specific to certain chart types. Split Series : When checked, an extra series is to be selected under the \u2018Split Series\u2019 dropdown. The series should contain a group label for each data point. All points with the same group value are plotted as a separate line. For example: Invert Y-axis : When checked the Y axis is flipped, with smaller values above and larger values below. Connect gaps [for Line Charts only]: When checked, gaps caused by missing values are connected by connecting neighboring points. The \u201cShow Markers\u201d option described next can be used to keep a visual cue for which points are present. Show markers [for Line Charts only]: When checked, each point on the line is marked additionally by a small circle. See the example for Split Series above. Stack series [for Line and Bar Charts]: When checked, split series will be stacked, rather than shown separately, giving a total for your selected series. In this example, we can see the total revenue for each month across all three departments. Note that \u2018Split series\u2019 must be checked in order to select multiple series to stack. Error bars [for Line and Bar Charts]: When set to \u201cSymmetric\u201d, each Y series should be followed by a series for the length of the error bars to show. When set to \u201cAbove+Below\u201d, each Y series should be followed by two series, one for the top error bars, and one for the bottom. In the example here, \u201cSplit Series\u201d is checked. So the series selected from the Split Series dropdown (\u201cCell Line\u201d), specifies how to group the data into lines. The series selected under the X-Axis dropdown (\u201cLog[Drug], uM\u201d), specifies our values along the x-axis. Our first series in the Series list (\u201c% Viable Cells\u201d), specifies our y-axis values, and the series that follows that (\u201cSD\u201d), specifies the error bars for those Y values.","title":"Chart"},{"location":"widget-chart/#page-widget-chart","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist supports several chart types to help you visualize your data easily. Charts may be used to plot a regular table of data, a linked widget (as described in Linking widgets ), or a summary table (as described in Summary tables ). The most common chart types are illustrated here:","title":"Page widget: Chart"},{"location":"widget-chart/#chart-types","text":"Each chart type plots one or several data series. Select the series to plot by clicking the green \u2018Add Series\u2019 text in the creator panel . In the area above the \u201cSeries\u201d section of the creator panel, you may configure the x-axis for most charts, or the labels for pie and donut charts.","title":"Chart types"},{"location":"widget-chart/#bar-chart","text":"Needs an x-axis and at least one series to plot along the y-axis. Additional series create additional bars at each point on the x-axis. To stack series onto the same bar, select the \u201cStack series\u201d checkbox.","title":"Bar Chart"},{"location":"widget-chart/#line-chart","text":"Needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines.","title":"Line Chart"},{"location":"widget-chart/#pie-chart","text":"Needs pie slice labels and one series for the pie slice sizes.","title":"Pie Chart"},{"location":"widget-chart/#area-chart","text":"Similar to a line chart, needs an x-axis and at least one series to plot along the y-axis. Additional series specify Y values for additional lines.","title":"Area Chart"},{"location":"widget-chart/#scatter-plot","text":"Needs a label and two or more series. The label applies to the points. The series apply the X and Y values for each point, respectively. Additional series specify Y values for additional sets of points.","title":"Scatter Plot"},{"location":"widget-chart/#kaplan-meier-plot","text":"The Kaplan-Meier Plot is useful for certain studies, and needs one label and one series. The label applies to the lines being plotted. The series gives a survival time or time-to-failure of that point. The plot shows the survival times on the X axis, and the number of points that survive at that time on the Y axis.","title":"Kaplan-Meier Plot"},{"location":"widget-chart/#chart-options","text":"A number of chart options are available, some of them specific to certain chart types. Split Series : When checked, an extra series is to be selected under the \u2018Split Series\u2019 dropdown. The series should contain a group label for each data point. All points with the same group value are plotted as a separate line. For example: Invert Y-axis : When checked the Y axis is flipped, with smaller values above and larger values below. Connect gaps [for Line Charts only]: When checked, gaps caused by missing values are connected by connecting neighboring points. The \u201cShow Markers\u201d option described next can be used to keep a visual cue for which points are present. Show markers [for Line Charts only]: When checked, each point on the line is marked additionally by a small circle. See the example for Split Series above. Stack series [for Line and Bar Charts]: When checked, split series will be stacked, rather than shown separately, giving a total for your selected series. In this example, we can see the total revenue for each month across all three departments. Note that \u2018Split series\u2019 must be checked in order to select multiple series to stack. Error bars [for Line and Bar Charts]: When set to \u201cSymmetric\u201d, each Y series should be followed by a series for the length of the error bars to show. When set to \u201cAbove+Below\u201d, each Y series should be followed by two series, one for the top error bars, and one for the bottom. In the example here, \u201cSplit Series\u201d is checked. So the series selected from the Split Series dropdown (\u201cCell Line\u201d), specifies how to group the data into lines. The series selected under the X-Axis dropdown (\u201cLog[Drug], uM\u201d), specifies our values along the x-axis. Our first series in the Series list (\u201c% Viable Cells\u201d), specifies our y-axis values, and the series that follows that (\u201cSD\u201d), specifies the error bars for those Y values.","title":"Chart options"},{"location":"widget-calendar/","text":"Page widget: Calendar # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The calendar widget allows viewing data in a calendar view. In Grist, event data is stored in a data table. Then, the calendar widget can be configured to display that data. Setting up your data # In your data table, you will need two columns of data, with the option to add three additional data columns: Title : Text column, containing the title of your event. Start Date : Date or DateTime column, containing the date, or date and time, that the event begins. (Optional) End Date : Date or DateTime column, containing the date, or date and time, that the event ends. (Optional) Is All Day? : Toggle column, noting if an event is all day long. (Optional) Type : Choice or Choice List column, containing the event category and styling. Configuring the calendar # Add a calendar widget from the \u2018Add New\u2019 menu. Select the table containing your event data. Configure the widget by selecting the columns in your data table that contain Start Date, End Date (optional), Is All Day? (optional), Title and Type (optional). Adding a new event # You can add a new event by double-clicking the start time for the event in the calendar widget. In the popup, you can add a title for the event and modify the start and end time. You also have the option to mark the event as \u2018all day\u2019. You can also modify the start and end time of an event by clicking and dragging the event directly on the calendar. To modify an event\u2019s start time, click and drag from the middle of the event. When modifying start time, the duration of the event will remain the same. To modify an event\u2019s end time, click and drag from the bottom of the event. You\u2019ll notice that the icon is slightly different from the icon that appears when modifying start time. Any changes to start and end time will be made to the underlying data table so your data will always be consistent! Linking event details # It might be useful to see more event details in a table or card widget. This example will walk through a card widget. To create a linked view, add a new type of widget such as a table or a card, and select the same data table. Under \u2018Select By\u2019, select the calendar widget. Then, add to page. Now, when you click on an event in the calendar widget, the linked widget will update to show the details for the selected event. Collapsing widgets Drag the linked widget into the attic at the top of the page to collapse. The widget will still be linked but will take up less space on the page! Simply click the box to expand and view linked record details. Read more on Custom Layouts . Deleting an event # To delete an event, double-click the event in the calendar then select \u2018delete\u2019 in the popup.","title":"Calendar"},{"location":"widget-calendar/#page-widget-calendar","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The calendar widget allows viewing data in a calendar view. In Grist, event data is stored in a data table. Then, the calendar widget can be configured to display that data.","title":"Page widget: Calendar"},{"location":"widget-calendar/#setting-up-your-data","text":"In your data table, you will need two columns of data, with the option to add three additional data columns: Title : Text column, containing the title of your event. Start Date : Date or DateTime column, containing the date, or date and time, that the event begins. (Optional) End Date : Date or DateTime column, containing the date, or date and time, that the event ends. (Optional) Is All Day? : Toggle column, noting if an event is all day long. (Optional) Type : Choice or Choice List column, containing the event category and styling.","title":"Setting up your data"},{"location":"widget-calendar/#configuring-the-calendar","text":"Add a calendar widget from the \u2018Add New\u2019 menu. Select the table containing your event data. Configure the widget by selecting the columns in your data table that contain Start Date, End Date (optional), Is All Day? (optional), Title and Type (optional).","title":"Configuring the calendar"},{"location":"widget-calendar/#adding-a-new-event","text":"You can add a new event by double-clicking the start time for the event in the calendar widget. In the popup, you can add a title for the event and modify the start and end time. You also have the option to mark the event as \u2018all day\u2019. You can also modify the start and end time of an event by clicking and dragging the event directly on the calendar. To modify an event\u2019s start time, click and drag from the middle of the event. When modifying start time, the duration of the event will remain the same. To modify an event\u2019s end time, click and drag from the bottom of the event. You\u2019ll notice that the icon is slightly different from the icon that appears when modifying start time. Any changes to start and end time will be made to the underlying data table so your data will always be consistent!","title":"Adding a new event"},{"location":"widget-calendar/#linking-event-details","text":"It might be useful to see more event details in a table or card widget. This example will walk through a card widget. To create a linked view, add a new type of widget such as a table or a card, and select the same data table. Under \u2018Select By\u2019, select the calendar widget. Then, add to page. Now, when you click on an event in the calendar widget, the linked widget will update to show the details for the selected event. Collapsing widgets Drag the linked widget into the attic at the top of the page to collapse. The widget will still be linked but will take up less space on the page! Simply click the box to expand and view linked record details. Read more on Custom Layouts .","title":"Linking event details"},{"location":"widget-calendar/#deleting-an-event","text":"To delete an event, double-click the event in the calendar then select \u2018delete\u2019 in the popup.","title":"Deleting an event"},{"location":"widget-custom/","text":"Page widget: Custom # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The Custom widget allows a user to insert almost anything in their document. To create a custom widget currently requires knowledge of web development, and access to a public web server (for example, GitHub Pages). A good use for custom widgets is to view records or tables in new ways. Using Grist as your data model and modern HTML/CSS/JS as your view is very powerful. Minimal example # To demonstrate to a web developer how custom widgets work, there is a minimal working example at: https://public.getgrist.com/911KcgKA95oQ/Minimal-Custom-Widget/m/fork The example shows a table with some random data (names for pets), and two custom widgets, one showing the selected row in the table as JSON, and the other showing all rows of the table as JSON. If you change data in the table, or move the cursor, the custom widgets update as appropriate. The source code for the widgets is at: https://github.com/gristlabs/grist-widget/tree/master/inspect It is stripped down to the essentials. Here is the full source code of the onRecord widget that shows one row of data:
Waiting for data...The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support. Adding a custom widget # To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html Access level # When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget. Invoice example # The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord . Creating a custom widget # As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API. Column mapping # Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping. Widget options # If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen. Custom Widget linking # Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'}); Premade Custom Widgets # All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown. Advanced Charts # The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration. Copy to clipboard # Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Dropbox Embedder # View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template. Grist Video Player # Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget. HTML Viewer # The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Image Viewer # View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar ! JupyterLite Notebook # This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
Waiting for data...The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support.","title":"Minimal example"},{"location":"widget-custom/#adding-a-custom-widget","text":"To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html","title":"Adding a custom widget"},{"location":"widget-custom/#access-level","text":"When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget.","title":"Access level"},{"location":"widget-custom/#invoice-example","text":"The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord .","title":"Invoice example"},{"location":"widget-custom/#creating-a-custom-widget","text":"As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API.","title":"Creating a custom widget"},{"location":"widget-custom/#column-mapping","text":"Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping.","title":"Column mapping"},{"location":"widget-custom/#widget-options","text":"If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen.","title":"Widget options"},{"location":"widget-custom/#custom-widget-linking","text":"Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'});","title":"Custom Widget linking"},{"location":"widget-custom/#premade-custom-widgets","text":"All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown.","title":"Premade Custom Widgets"},{"location":"widget-custom/#advanced-charts","text":"The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration.","title":"Advanced Charts"},{"location":"widget-custom/#copy-to-clipboard","text":"Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"Copy to clipboard"},{"location":"widget-custom/#dropbox-embedder","text":"View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template.","title":"Dropbox Embedder"},{"location":"widget-custom/#grist-video-player","text":"Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget.","title":"Grist Video Player"},{"location":"widget-custom/#html-viewer","text":"The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"HTML Viewer"},{"location":"widget-custom/#image-viewer","text":"View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar !","title":"Image Viewer"},{"location":"widget-custom/#jupyterlite-notebook","text":"This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
DateTime:America/New_York
, Ref:Users
\",\"enum\":[\"Any\",\"Text\",\"Numeric\",\"Int\",\"Bool\",\"Date\",\"DateTime:$A + Table1.lookupOne(B=$B)
\"},\"isFormula\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column is a formula column. Use \\\"false\\\" for trigger formula column.\"},\"widgetOptions\":{\"type\":\"string\",\"description\":\"A JSON object with widget options, e.g.: {\\\"choices\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"alignment\\\": \\\"right\\\"}
\"},\"untieColIdFromLabel\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column label should not be used as the column identifier. Use \\\"false\\\" to use the label as the identifier.\"},\"recalcWhen\":{\"type\":\"integer\",\"description\":\"A number indicating when the column should be recalculated. [2, 3]
\"}}}]},\"GetFields\":{\"allOf\":[{\"$ref\":\"#/components/schemas/Fields\"},{\"type\":\"object\",\"properties\":{\"recalcDeps\":{\"type\":\"array\",\"items\":{\"type\":\"integer\"},\"description\":\"An array of column identifiers (colRefs) that this column depends on, prefixed with \\\"L\\\" constant. If any of these columns change, the column will be recalculated. E.g.: [\\\"L\\\", 2, 3]
\"},\"colRef\":{\"type\":\"integer\",\"description\":\"Column reference, e.g.: 2
\"}}}]},\"RowIds\":{\"type\":\"array\",\"example\":[101,102,103],\"items\":{\"type\":\"integer\"}},\"DocParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Competitive Analysis\"},\"isPinned\":{\"type\":\"boolean\",\"example\":false}}},\"WorkspaceParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Retreat Docs\"}}},\"OrgParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"ACME Unlimited\"}}},\"OrgAccessRead\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"OrgAccessWrite\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"WorkspaceAccessRead\":{\"type\":\"object\",\"required\":[\"maxInheritedRole\",\"users\"],\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"},\"parentAccess\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"WorkspaceAccessWrite\":{\"type\":\"object\",\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"DocAccessWrite\":{\"$ref\":\"#/components/schemas/WorkspaceAccessWrite\"},\"DocAccessRead\":{\"$ref\":\"#/components/schemas/WorkspaceAccessRead\"},\"AttachmentUpload\":{\"type\":\"object\",\"properties\":{\"upload\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"format\":\"binary\"}}}},\"AttachmentId\":{\"type\":\"number\",\"description\":\"An integer ID\"},\"AttachmentMetadata\":{\"type\":\"object\",\"properties\":{\"fileName\":{\"type\":\"string\",\"example\":\"logo.png\"},\"fileSize\":{\"type\":\"number\",\"example\":12345},\"timeUploaded\":{\"type\":\"string\",\"example\":\"2020-02-13T12:17:19.000Z\"}}},\"AttachmentMetadataList\":{\"type\":\"object\",\"required\":[\"records\"],\"properties\":{\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"fields\"],\"properties\":{\"id\":{\"type\":\"number\",\"example\":1},\"fields\":{\"$ref\":\"#/components/schemas/AttachmentMetadata\"}}}}}},\"SqlResultSet\":{\"type\":\"object\",\"required\":[\"statement\",\"records\"],\"properties\":{\"statement\":{\"type\":\"string\",\"description\":\"A copy of the SQL statement.\",\"example\":\"select * from Pets ...\"},\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"object\"}}},\"example\":[{\"fields\":{\"id\":1,\"pet\":\"cat\",\"popularity\":67}},{\"fields\":{\"id\":2,\"pet\":\"dog\",\"popularity\":95}}]}}},\"TableSchemaResult\":{\"type\":\"object\",\"required\":[\"name\",\"title\",\"schema\"],\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"The ID (technical name) of the table\"},\"title\":{\"type\":\"string\",\"description\":\"The human readable name of the table\"},\"path\":{\"type\":\"string\",\"description\":\"The URL to download the CSV\",\"example\":\"https://getgrist.com/o/docs/api/docs/ID/download/csv?tableId=Table1&....\"},\"format\":{\"type\":\"string\",\"enum\":[\"csv\"]},\"mediatype\":{\"type\":\"string\",\"enum\":[\"text/csv\"]},\"encoding\":{\"type\":\"string\",\"enum\":[\"utf-8\"]},\"dialect\":{\"$ref\":\"#/components/schemas/csv-dialect\"},\"schema\":{\"$ref\":\"#/components/schemas/table-schema\"}}},\"csv-dialect\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"CSV Dialect\",\"description\":\"The CSV dialect descriptor.\",\"type\":[\"string\",\"object\"],\"required\":[\"delimiter\",\"doubleQuote\"],\"properties\":{\"csvddfVersion\":{\"title\":\"CSV Dialect schema version\",\"description\":\"A number to indicate the schema version of CSV Dialect. Version 1.0 was named CSV Dialect Description Format and used different field names.\",\"type\":\"number\",\"default\":1.2,\"examples:\":[\"{\\n \\\"csvddfVersion\\\": \\\"1.2\\\"\\n}\\n\"]},\"delimiter\":{\"title\":\"Delimiter\",\"description\":\"A character sequence to use as the field separator.\",\"type\":\"string\",\"default\":\",\",\"examples\":[\"{\\n \\\"delimiter\\\": \\\",\\\"\\n}\\n\",\"{\\n \\\"delimiter\\\": \\\";\\\"\\n}\\n\"]},\"doubleQuote\":{\"title\":\"Double Quote\",\"description\":\"Specifies the handling of quotes inside fields.\",\"context\":\"If Double Quote is set to true, two consecutive quotes must be interpreted as one.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"doubleQuote\\\": true\\n}\\n\"]},\"lineTerminator\":{\"title\":\"Line Terminator\",\"description\":\"Specifies the character sequence that must be used to terminate rows.\",\"type\":\"string\",\"default\":\"\\r\\n\",\"examples\":[\"{\\n \\\"lineTerminator\\\": \\\"\\\\r\\\\n\\\"\\n}\\n\",\"{\\n \\\"lineTerminator\\\": \\\"\\\\n\\\"\\n}\\n\"]},\"nullSequence\":{\"title\":\"Null Sequence\",\"description\":\"Specifies the null sequence, for example, `\\\\N`.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"nullSequence\\\": \\\"\\\\N\\\"\\n}\\n\"]},\"quoteChar\":{\"title\":\"Quote Character\",\"description\":\"Specifies a one-character string to use as the quoting character.\",\"type\":\"string\",\"default\":\"\\\"\",\"examples\":[\"{\\n \\\"quoteChar\\\": \\\"'\\\"\\n}\\n\"]},\"escapeChar\":{\"title\":\"Escape Character\",\"description\":\"Specifies a one-character string to use as the escape character.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"escapeChar\\\": \\\"\\\\\\\\\\\"\\n}\\n\"]},\"skipInitialSpace\":{\"title\":\"Skip Initial Space\",\"description\":\"Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"skipInitialSpace\\\": true\\n}\\n\"]},\"header\":{\"title\":\"Header\",\"description\":\"Specifies if the file includes a header row, always as the first row in the file.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"header\\\": true\\n}\\n\"]},\"commentChar\":{\"title\":\"Comment Character\",\"description\":\"Specifies that any row beginning with this one-character string, without preceeding whitespace, causes the entire line to be ignored.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"commentChar\\\": \\\"#\\\"\\n}\\n\"]},\"caseSensitiveHeader\":{\"title\":\"Case Sensitive Header\",\"description\":\"Specifies if the case of headers is meaningful.\",\"context\":\"Use of case in source CSV files is not always an intentional decision. For example, should \\\"CAT\\\" and \\\"Cat\\\" be considered to have the same meaning.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"caseSensitiveHeader\\\": true\\n}\\n\"]}},\"examples\":[\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\";\\\"\\n }\\n}\\n\",\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\"\\\\t\\\",\\n \\\"quoteChar\\\": \\\"'\\\",\\n \\\"commentChar\\\": \\\"#\\\"\\n }\\n}\\n\"]},\"table-schema\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Table Schema\",\"description\":\"A Table Schema for this resource, compliant with the [Table Schema](./tableschema/) specification.\",\"type\":[\"string\",\"object\"],\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Field\",\"type\":\"object\",\"oneOf\":[{\"type\":\"object\",\"title\":\"String Field\",\"description\":\"The field contains strings, that is, sequences of characters.\",\"required\":[\"name\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `string`.\",\"enum\":[\"string\"]},\"format\":{\"description\":\"The format keyword options for `string` are `default`, `email`, `uri`, `binary`, and `uuid`.\",\"context\":\"The following `format` options are supported:\\n * **default**: any valid string.\\n * **email**: A valid email address.\\n * **uri**: A valid URI.\\n * **binary**: A base64 encoded string representing binary data.\\n * **uuid**: A string that is a uuid.\",\"enum\":[\"default\",\"email\",\"uri\",\"binary\",\"uuid\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `string` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"pattern\":{\"type\":\"string\",\"description\":\"A regular expression pattern to test each value of the property against, where a truthy response indicates validity.\",\"context\":\"Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs).\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"constraints\\\": {\\n \\\"minLength\\\": 3,\\n \\\"maxLength\\\": 35\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Number Field\",\"description\":\"The field contains numbers of any kind including decimals.\",\"context\":\"The lexical formatting follows that of decimal in [XMLSchema](https://www.w3.org/TR/xmlschema-2/#decimal): a non-empty finite-length sequence of decimal digits separated by a period as a decimal indicator. An optional leading sign is allowed. If the sign is omitted, '+' is assumed. Leading and trailing zeroes are optional. If the fractional part is zero, the period and following zero(es) can be omitted. For example: '-1.23', '12678967.543233', '+100000.00', '210'.\\n\\nThe following special string values are permitted (case does not need to be respected):\\n - NaN: not a number\\n - INF: positive infinity\\n - -INF: negative infinity\\n\\nA number `MAY` also have a trailing:\\n - exponent: this `MUST` consist of an E followed by an optional + or - sign followed by one or more decimal digits (0-9)\\n - percentage: the percentage sign: `%`. In conversion percentages should be divided by 100.\\n\\nIf both exponent and percentages are present the percentage `MUST` follow the exponent e.g. '53E10%' (equals 5.3).\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `number`.\",\"enum\":[\"number\"]},\"format\":{\"description\":\"There are no format keyword options for `number`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"decimalChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to represent a decimal point within the number. The default value is `.`.\"},\"groupChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to group digits within the number. The default value is `null`. A common value is `,` e.g. '100,000'.\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `number` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"number\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\",\\n \\\"constraints\\\": {\\n \\\"enum\\\": [ \\\"1.00\\\", \\\"1.50\\\", \\\"2.00\\\" ]\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Integer Field\",\"description\":\"The field contains integers - that is whole numbers.\",\"context\":\"Integer values are indicated in the standard way for any valid integer.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `integer`.\",\"enum\":[\"integer\"]},\"format\":{\"description\":\"There are no format keyword options for `integer`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `integer` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\",\\n \\\"constraints\\\": {\\n \\\"unique\\\": true,\\n \\\"minimum\\\": 100,\\n \\\"maximum\\\": 9999\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Field\",\"description\":\"The field contains temporal date values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `date`.\",\"enum\":[\"date\"]},\"format\":{\"description\":\"The format keyword options for `date` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string of YYYY-MM-DD.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `date` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": \\\"01-01-1900\\\"\\n }\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"format\\\": \\\"MM-DD-YYYY\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Time Field\",\"description\":\"The field contains temporal time values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `time`.\",\"enum\":[\"time\"]},\"format\":{\"description\":\"The format keyword options for `time` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for time.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `time` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\",\\n \\\"format\\\": \\\"any\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Time Field\",\"description\":\"The field contains temporal datetime values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `datetime`.\",\"enum\":[\"datetime\"]},\"format\":{\"description\":\"The format keyword options for `datetime` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for datetime.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `datetime` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\",\\n \\\"format\\\": \\\"default\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Field\",\"description\":\"A calendar year, being an integer with 4 digits. Equivalent to [gYear in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYear)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `year`.\",\"enum\":[\"year\"]},\"format\":{\"description\":\"There are no format keyword options for `year`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `year` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1970,\\n \\\"maximum\\\": 2003\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Month Field\",\"description\":\"A calendar year month, being an integer with 1 or 2 digits. Equivalent to [gYearMonth in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYearMonth)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `yearmonth`.\",\"enum\":[\"yearmonth\"]},\"format\":{\"description\":\"There are no format keyword options for `yearmonth`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `yearmonth` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1,\\n \\\"maximum\\\": 6\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Boolean Field\",\"description\":\"The field contains boolean (true/false) data.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `boolean`.\",\"enum\":[\"boolean\"]},\"format\":{\"description\":\"There are no format keyword options for `boolean`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"trueValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"true\",\"True\",\"TRUE\",\"1\"]},\"falseValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"false\",\"False\",\"FALSE\",\"0\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `boolean` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"boolean\"}}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"registered\\\",\\n \\\"type\\\": \\\"boolean\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Object Field\",\"description\":\"The field contains data which can be parsed as a valid JSON object.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `object`.\",\"enum\":[\"object\"]},\"format\":{\"description\":\"There are no format keyword options for `object`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `object` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"extra\\\"\\n \\\"type\\\": \\\"object\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoPoint Field\",\"description\":\"The field contains data describing a geographic point.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geopoint`.\",\"enum\":[\"geopoint\"]},\"format\":{\"description\":\"The format keyword options for `geopoint` are `default`,`array`, and `object`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A string of the pattern 'lon, lat', where `lon` is the longitude and `lat` is the latitude.\\n * **array**: An array of exactly two items, where each item is either a number, or a string parsable as a number, and the first item is `lon` and the second item is `lat`.\\n * **object**: A JSON object with exactly two keys, `lat` and `lon`\",\"notes\":[\"Implementations `MUST` strip all white space in the default format of `lon, lat`.\"],\"enum\":[\"default\",\"array\",\"object\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geopoint` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\",\\n \\\"format\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoJSON Field\",\"description\":\"The field contains a JSON object according to GeoJSON or TopoJSON\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geojson`.\",\"enum\":[\"geojson\"]},\"format\":{\"description\":\"The format keyword options for `geojson` are `default` and `topojson`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A geojson object as per the [GeoJSON spec](http://geojson.org/).\\n * **topojson**: A topojson object as per the [TopoJSON spec](https://github.com/topojson/topojson-specification/blob/master/README.md)\",\"enum\":[\"default\",\"topojson\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geojson` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\",\\n \\\"format\\\": \\\"topojson\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Array Field\",\"description\":\"The field contains data which can be parsed as a valid JSON array.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `array`.\",\"enum\":[\"array\"]},\"format\":{\"description\":\"There are no format keyword options for `array`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `array` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"options\\\"\\n \\\"type\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Duration Field\",\"description\":\"The field contains a duration of time.\",\"context\":\"The lexical representation for duration is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) extended format `PnYnMnDTnHnMnS`, where `nY` represents the number of years, `nM` the number of months, `nD` the number of days, 'T' is the date/time separator, `nH` the number of hours, `nM` the number of minutes and `nS` the number of seconds. The number of seconds can include decimal digits to arbitrary precision. Date and time elements including their designator may be omitted if their value is zero, and lower order elements may also be omitted for reduced precision. Here we follow the definition of [XML Schema duration datatype](http://www.w3.org/TR/xmlschema-2/#duration) directly and that definition is implicitly inlined here.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `duration`.\",\"enum\":[\"duration\"]},\"format\":{\"description\":\"There are no format keyword options for `duration`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `duration` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"period\\\"\\n \\\"type\\\": \\\"duration\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Any Field\",\"description\":\"Any value is accepted, including values that are not captured by the type/format/constraint requirements of the specification.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `any`.\",\"enum\":[\"any\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply to `any` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"notes\\\",\\n \\\"type\\\": \\\"any\\\"\\n\"]}]},\"description\":\"An `array` of Table Schema Field objects.\",\"examples\":[\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\"\\n }\\n ]\\n}\\n\",\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n },\\n {\\n \\\"name\\\": \\\"my-field-name-2\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n }\\n ]\\n}\\n\"]},\"primaryKey\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"string\"}],\"description\":\"A primary key is a field name or an array of field names, whose values `MUST` uniquely identify each row in the table.\",\"context\":\"Field name in the `primaryKey` `MUST` be unique, and `MUST` match a field name in the associated table. It is acceptable to have an array with a single value, indicating that the value of a single field is the primary key.\",\"examples\":[\"{\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n}\\n\",\"{\\n \\\"primaryKey\\\": [\\n \\\"first_name\\\",\\n \\\"last_name\\\"\\n ]\\n}\\n\"]},\"foreignKeys\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Foreign Key\",\"description\":\"Table Schema Foreign Key\",\"type\":\"object\",\"required\":[\"fields\",\"reference\"],\"oneOf\":[{\"properties\":{\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"minItems\":1,\"uniqueItems\":true,\"description\":\"Fields that make up the primary key.\"}},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"minItems\":1,\"uniqueItems\":true}}}}},{\"properties\":{\"fields\":{\"type\":\"string\",\"description\":\"Fields that make up the primary key.\"},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"string\"}}}}}]},\"examples\":[\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"the-resource\\\",\\n \\\"fields\\\": \\\"state_id\\\"\\n }\\n }\\n ]\\n}\\n\",\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"\\\",\\n \\\"fields\\\": \\\"id\\\"\\n }\\n }\\n ]\\n}\\n\"]},\"missingValues\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"default\":[\"\"],\"description\":\"Values that when encountered in the source, should be considered as `null`, 'not present', or 'blank' values.\",\"context\":\"Many datasets arrive with missing data values, either because a value was not collected or it never existed.\\nMissing values may be indicated simply by the value being empty in other cases a special value may have been used e.g. `-`, `NaN`, `0`, `-9999` etc.\\nThe `missingValues` property provides a way to indicate that these values should be interpreted as equivalent to null.\\n\\n`missingValues` are strings rather than being the data type of the particular field. This allows for comparison prior to casting and for fields to have missing value which are not of their type, for example a `number` field to have missing values indicated by `-`.\\n\\nThe default value of `missingValue` for a non-string type field is the empty string `''`. For string type fields there is no default for `missingValue` (for string fields the empty string `''` is a valid value and need not indicate null).\",\"examples\":[\"{\\n \\\"missingValues\\\": [\\n \\\"-\\\",\\n \\\"NaN\\\",\\n \\\"\\\"\\n ]\\n}\\n\",\"{\\n \\\"missingValues\\\": []\\n}\\n\"]}},\"examples\":[\"{\\n \\\"schema\\\": {\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"first_name\\\",\\n \\\"type\\\": \\\"string\\\"\\n \\\"constraints\\\": {\\n \\\"required\\\": true\\n }\\n },\\n {\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\"\\n },\\n ],\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n }\\n}\\n\"]}},\"parameters\":{\"filterQueryParam\":{\"in\":\"query\",\"name\":\"filter\",\"schema\":{\"type\":\"string\"},\"description\":\"This is a JSON object mapping column names to arrays of allowed values. For example, to filter column `pet` for values `cat` and `dog`, the filter would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}`. JSON contains characters that are not safe to place in a URL, so it is important to url-encode them. For this example, the url-encoding is `%7B%22pet%22%3A%20%5B%22cat%22%2C%20%22dog%22%5D%7D`. See https://rosettacode.org/wiki/URL_encoding for how to url-encode a string, or https://www.urlencoder.org/ to try some examples. Multiple columns can be filtered. For example the filter for `pet` being either `cat` or `dog`, AND `size` being either `tiny` or `outrageously small`, would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"size\\\": [\\\"tiny\\\", \\\"outrageously small\\\"]}`.\",\"example\":\"{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}\",\"required\":false},\"sortQueryParam\":{\"in\":\"query\",\"name\":\"sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Order in which to return results. If a single column name is given (e.g. `pet`), results are placed in ascending order of values in that column. To get results in an order that was previously prepared manually in Grist, use the special `manualSort` column name. Multiple columns can be specified, separated by commas (e.g. `pet,age`). For descending order, prefix a column name with a `-` character (e.g. `pet,-age`). To include additional sorting options append them after a colon (e.g. `pet,-age:naturalSort;emptyFirst,owner`). Available options are: `choiceOrder`, `naturalSort`, `emptyFirst`. Without the `sort` parameter, the order of results is unspecified.\",\"example\":\"pet,-age\",\"required\":false},\"limitQueryParam\":{\"in\":\"query\",\"name\":\"limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Return at most this number of rows. A value of 0 is equivalent to having no limit.\",\"example\":\"5\",\"required\":false},\"sortHeaderParam\":{\"in\":\"header\",\"name\":\"X-Sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Same as `sort` query parameter.\",\"example\":\"pet,-age\",\"required\":false},\"limitHeaderParam\":{\"in\":\"header\",\"name\":\"X-Limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Same as `limit` query parameter.\",\"example\":\"5\",\"required\":false},\"colIdPathParam\":{\"in\":\"path\",\"name\":\"colId\",\"schema\":{\"type\":\"string\"},\"description\":\"The column id (without the starting `$`) as shown in the column configuration below the label\",\"required\":true},\"tableIdPathParam\":{\"in\":\"path\",\"name\":\"tableId\",\"schema\":{\"type\":\"string\"},\"description\":\"normalized table name (see `TABLE ID` in Raw Data) or numeric row ID in `_grist_Tables`\",\"required\":true},\"docIdPathParam\":{\"in\":\"path\",\"name\":\"docId\",\"schema\":{\"type\":\"string\"},\"description\":\"A string id (UUID)\",\"required\":true},\"orgIdPathParam\":{\"in\":\"path\",\"name\":\"orgId\",\"schema\":{\"oneOf\":[{\"type\":\"integer\"},{\"type\":\"string\"}]},\"description\":\"This can be an integer id, or a string subdomain (e.g. `gristlabs`), or `current` if the org is implied by the domain in the url\",\"required\":true},\"workspaceIdPathParam\":{\"in\":\"path\",\"name\":\"workspaceId\",\"description\":\"An integer id\",\"schema\":{\"type\":\"integer\"},\"required\":true},\"noparseQueryParam\":{\"in\":\"query\",\"name\":\"noparse\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to prohibit parsing strings according to the column type.\"},\"hiddenQueryParam\":{\"in\":\"query\",\"name\":\"hidden\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to include the hidden columns (like \\\"manualSort\\\")\"},\"headerQueryParam\":{\"in\":\"query\",\"name\":\"header\",\"schema\":{\"type\":\"string\",\"enum\":[\"colId\",\"label\"]},\"description\":\"Format for headers. Labels tend to be more human-friendly while colIds are more normalized.\",\"required\":false}}}}},\"searchIndex\":{\"store\":[\"section/Authentication\",\"tag/orgs\",\"tag/orgs/operation/listOrgs\",\"tag/orgs/operation/describeOrg\",\"tag/orgs/operation/modifyOrg\",\"tag/orgs/operation/listOrgAccess\",\"tag/orgs/operation/modifyOrgAccess\",\"tag/workspaces\",\"tag/workspaces/operation/listWorkspaces\",\"tag/workspaces/operation/createWorkspace\",\"tag/workspaces/operation/describeWorkspace\",\"tag/workspaces/operation/modifyWorkspace\",\"tag/workspaces/operation/deleteWorkspace\",\"tag/workspaces/operation/listWorkspaceAccess\",\"tag/workspaces/operation/modifyWorkspaceAccess\",\"tag/docs\",\"tag/docs/operation/createDoc\",\"tag/docs/operation/describeDoc\",\"tag/docs/operation/modifyDoc\",\"tag/docs/operation/deleteDoc\",\"tag/docs/operation/moveDoc\",\"tag/docs/operation/listDocAccess\",\"tag/docs/operation/modifyDocAccess\",\"tag/docs/operation/downloadDoc\",\"tag/docs/operation/downloadDocXlsx\",\"tag/docs/operation/downloadDocCsv\",\"tag/docs/operation/downloadTableSchema\",\"tag/records\",\"tag/records/operation/listRecords\",\"tag/records/operation/addRecords\",\"tag/records/operation/modifyRecords\",\"tag/records/operation/replaceRecords\",\"tag/tables\",\"tag/tables/operation/listTables\",\"tag/tables/operation/addTables\",\"tag/tables/operation/modifyTables\",\"tag/columns\",\"tag/columns/operation/listColumns\",\"tag/columns/operation/addColumns\",\"tag/columns/operation/modifyColumns\",\"tag/columns/operation/replaceColumns\",\"tag/columns/operation/deleteColumn\",\"tag/data\",\"tag/data/operation/getTableData\",\"tag/data/operation/addRows\",\"tag/data/operation/modifyRows\",\"tag/data/operation/deleteRows\",\"tag/attachments\",\"tag/attachments/operation/listAttachments\",\"tag/attachments/operation/uploadAttachments\",\"tag/attachments/operation/getAttachmentMetadata\",\"tag/attachments/operation/downloadAttachment\",\"tag/webhooks\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/get\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/post\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/patch\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/delete\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1queue/delete\",\"tag/sql\",\"tag/sql/paths/~1docs~1{docId}~1sql/get\",\"tag/sql/paths/~1docs~1{docId}~1sql/post\"],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"title\",\"description\"],\"fieldVectors\":[[\"title/0\",[0,5.068]],[\"description/0\",[1,4.208,2,4.208]],[\"title/1\",[3,2.418]],[\"description/1\",[3,1.097,4,1.984,5,1.776,6,1.984,7,2.3,8,1.621,9,2.3]],[\"title/2\",[3,1.739,10,1.946,11,2.068]],[\"description/2\",[3,1.097,4,1.984,5,1.776,6,1.984,12,2.3,13,2.3,14,2.3]],[\"title/3\",[3,2.023,15,3.275]],[\"description/3\",[16,4.353]],[\"title/4\",[3,2.023,17,2.264]],[\"description/4\",[16,4.353]],[\"title/5\",[3,1.525,10,1.706,11,1.814,18,2.468]],[\"description/5\",[19,4.353]],[\"title/6\",[3,1.739,11,2.068,20,2.569]],[\"description/6\",[19,4.353]],[\"title/7\",[21,2.294]],[\"description/7\",[5,1.953,8,1.782,21,1.145,22,2.529,23,2.529,24,0.689]],[\"title/8\",[3,1.358,10,1.519,21,1.288,24,0.775,25,2.845]],[\"description/8\",[26,4.353]],[\"title/9\",[21,1.65,27,2.815,28,2.815]],[\"description/9\",[26,4.353]],[\"title/10\",[15,3.275,21,1.919]],[\"description/10\",[29,3.896]],[\"title/11\",[17,2.264,21,1.919]],[\"description/11\",[29,3.896]],[\"title/12\",[21,1.919,30,2.988]],[\"description/12\",[29,3.896]],[\"title/13\",[10,1.706,11,1.814,18,2.468,21,1.447]],[\"description/13\",[31,4.353]],[\"title/14\",[11,2.068,20,2.569,21,1.65]],[\"description/14\",[31,4.353]],[\"title/15\",[32,3.914]],[\"description/15\",[21,1.272,24,0.765,33,2.424,34,1.98,35,2.809]],[\"title/16\",[24,0.993,27,2.815,28,2.815]],[\"description/16\",[36,5.045]],[\"title/17\",[15,3.275,24,1.155]],[\"description/17\",[37,3.896]],[\"title/18\",[17,1.706,24,0.87,38,2.468,39,2.08]],[\"description/18\",[37,3.896]],[\"title/19\",[24,1.155,30,2.988]],[\"description/19\",[37,3.896]],[\"title/20\",[3,1.223,21,1.161,24,0.698,40,2.564,41,2.564,42,2.564]],[\"description/20\",[43,5.045]],[\"title/21\",[10,1.706,11,1.814,18,2.468,24,0.87]],[\"description/21\",[44,4.353]],[\"title/22\",[11,2.068,20,2.569,24,0.993]],[\"description/22\",[44,4.353]],[\"title/23\",[24,0.87,39,2.08,45,3.196,46,2.252]],[\"description/23\",[47,5.045]],[\"title/24\",[24,0.87,39,2.08,46,2.252,48,3.196]],[\"description/24\",[49,5.045]],[\"title/25\",[39,2.08,46,2.252,50,0.833,51,3.196]],[\"description/25\",[52,5.045]],[\"title/26\",[50,1.105,53,3.658]],[\"description/26\",[53,2.182,54,2.529,55,2.529,56,2.529,57,2.529,58,2.529]],[\"title/27\",[59,2.294]],[\"description/27\",[8,1.782,33,2.182,34,1.782,50,0.659,59,1.145,60,1.782]],[\"title/28\",[50,0.95,59,1.65,61,3.144]],[\"description/28\",[62,3.556]],[\"title/29\",[50,0.95,59,1.65,63,2.209]],[\"description/29\",[62,3.556]],[\"title/30\",[17,1.946,50,0.95,59,1.65]],[\"description/30\",[62,3.556]],[\"title/31\",[50,0.833,59,1.447,63,1.937,64,2.757]],[\"description/31\",[62,3.556]],[\"title/32\",[50,1.321]],[\"description/32\",[24,0.86,34,2.227,50,0.823,65,2.726]],[\"title/33\",[10,1.946,24,0.993,50,0.95]],[\"description/33\",[66,3.896]],[\"title/34\",[24,0.993,50,0.95,63,2.209]],[\"description/34\",[66,3.896]],[\"title/35\",[17,1.946,24,0.993,50,0.95]],[\"description/35\",[66,3.896]],[\"title/36\",[67,2.706]],[\"description/36\",[34,2.227,50,0.823,65,2.726,67,1.687]],[\"title/37\",[10,1.946,50,0.95,67,1.946]],[\"description/37\",[68,3.556]],[\"title/38\",[50,0.95,63,2.209,67,1.946]],[\"description/38\",[68,3.556]],[\"title/39\",[17,1.946,50,0.95,67,1.946]],[\"description/39\",[68,3.556]],[\"title/40\",[50,0.833,63,1.937,64,2.757,67,1.706]],[\"description/40\",[68,3.556]],[\"title/41\",[30,2.569,50,0.95,67,1.946]],[\"description/41\",[69,5.045]],[\"title/42\",[70,3.572]],[\"description/42\",[50,0.412,59,0.716,70,1.115,71,1.582,72,1.365,73,2.652,74,1.115,75,1.582,76,1.582,77,1.582,78,1.115]],[\"title/43\",[50,0.95,61,3.144,70,2.569]],[\"description/43\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/44\",[50,0.95,60,2.569,63,2.209]],[\"description/44\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/45\",[17,1.946,50,0.95,60,2.569]],[\"description/45\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/46\",[30,2.569,50,0.95,60,2.569]],[\"description/46\",[90,5.045]],[\"title/47\",[91,3.071]],[\"description/47\",[24,0.46,46,1.189,59,0.764,67,0.901,70,1.189,72,1.456,91,1.696,92,1.687,93,1.687,94,1.687]],[\"title/48\",[10,1.706,32,2.468,38,2.468,91,1.937]],[\"description/48\",[95,4.353]],[\"title/49\",[32,2.815,91,2.209,96,3.645]],[\"description/49\",[95,4.353]],[\"title/50\",[38,3.275,91,2.569]],[\"description/50\",[97,5.045]],[\"title/51\",[39,2.372,91,2.209,98,3.645]],[\"description/51\",[99,5.045]],[\"title/52\",[100,3.071]],[\"description/52\",[8,1.621,20,1.621,24,0.626,100,1.394,101,2.3,102,2.3,103,2.3]],[\"title/53\",[24,0.993,100,2.209,104,3.645]],[\"description/53\",[105,4.353]],[\"title/54\",[24,0.87,27,2.468,87,2.252,100,1.937]],[\"description/54\",[105,4.353]],[\"title/55\",[17,2.264,100,2.569]],[\"description/55\",[106,4.353]],[\"title/56\",[82,2.988,100,2.569]],[\"description/56\",[106,4.353]],[\"title/57\",[28,2.198,107,2.845,108,2.845,109,2.845,110,2.845]],[\"description/57\",[111,5.045]],[\"title/58\",[112,3.914]],[\"description/58\",[]],[\"title/59\",[24,0.775,112,2.198,113,2.455,114,2.455,115,2.455]],[\"description/59\",[116,4.353]],[\"title/60\",[24,0.636,112,1.802,113,2.013,114,2.013,115,2.013,117,2.334,118,2.334]],[\"description/60\",[116,4.353]]],\"invertedIndex\":[[\"\",{\"_index\":2,\"title\":{},\"description\":{\"0\":{}}}],[\"access\",{\"_index\":11,\"title\":{\"2\":{},\"5\":{},\"6\":{},\"13\":{},\"14\":{},\"21\":{},\"22\":{}},\"description\":{}}],[\"add\",{\"_index\":63,\"title\":{\"29\":{},\"31\":{},\"34\":{},\"38\":{},\"40\":{},\"44\":{}},\"description\":{}}],[\"against\",{\"_index\":115,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"anoth\",{\"_index\":41,\"title\":{\"20\":{}},\"description\":{}}],[\"api\",{\"_index\":9,\"title\":{},\"description\":{\"1\":{}}}],[\"area\",{\"_index\":13,\"title\":{},\"description\":{\"2\":{}}}],[\"associ\",{\"_index\":104,\"title\":{\"53\":{}},\"description\":{}}],[\"attach\",{\"_index\":91,\"title\":{\"47\":{},\"48\":{},\"49\":{},\"50\":{},\"51\":{}},\"description\":{\"47\":{}}}],[\"authent\",{\"_index\":0,\"title\":{\"0\":{}},\"description\":{}}],[\"avail\",{\"_index\":14,\"title\":{},\"description\":{\"2\":{}}}],[\"better\",{\"_index\":84,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"call\",{\"_index\":8,\"title\":{},\"description\":{\"1\":{},\"7\":{},\"27\":{},\"52\":{}}}],[\"chang\",{\"_index\":20,\"title\":{\"6\":{},\"14\":{},\"22\":{}},\"description\":{\"52\":{}}}],[\"collect\",{\"_index\":34,\"title\":{},\"description\":{\"15\":{},\"27\":{},\"32\":{},\"36\":{}}}],[\"column\",{\"_index\":67,\"title\":{\"36\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{}},\"description\":{\"36\":{},\"47\":{}}}],[\"columnar\",{\"_index\":75,\"title\":{},\"description\":{\"42\":{}}}],[\"consid\",{\"_index\":83,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"contain\",{\"_index\":33,\"title\":{},\"description\":{\"15\":{},\"27\":{}}}],[\"content\",{\"_index\":39,\"title\":{\"18\":{},\"23\":{},\"24\":{},\"25\":{},\"51\":{}},\"description\":{}}],[\"creat\",{\"_index\":27,\"title\":{\"9\":{},\"16\":{},\"54\":{}},\"description\":{}}],[\"csv\",{\"_index\":51,\"title\":{\"25\":{}},\"description\":{}}],[\"data\",{\"_index\":70,\"title\":{\"42\":{},\"43\":{}},\"description\":{\"42\":{},\"47\":{}}}],[\"delet\",{\"_index\":30,\"title\":{\"12\":{},\"19\":{},\"41\":{},\"46\":{}},\"description\":{}}],[\"deprec\",{\"_index\":74,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"describ\",{\"_index\":15,\"title\":{\"3\":{},\"10\":{},\"17\":{}},\"description\":{}}],[\"doc\",{\"_index\":32,\"title\":{\"15\":{},\"48\":{},\"49\":{}},\"description\":{}}],[\"docs/{docid\",{\"_index\":37,\"title\":{},\"description\":{\"17\":{},\"18\":{},\"19\":{}}}],[\"docs/{docid}/access\",{\"_index\":44,\"title\":{},\"description\":{\"21\":{},\"22\":{}}}],[\"docs/{docid}/attach\",{\"_index\":95,\"title\":{},\"description\":{\"48\":{},\"49\":{}}}],[\"docs/{docid}/attachments/{attachmentid\",{\"_index\":97,\"title\":{},\"description\":{\"50\":{}}}],[\"docs/{docid}/attachments/{attachmentid}/download\",{\"_index\":99,\"title\":{},\"description\":{\"51\":{}}}],[\"docs/{docid}/download\",{\"_index\":47,\"title\":{},\"description\":{\"23\":{}}}],[\"docs/{docid}/download/csv\",{\"_index\":52,\"title\":{},\"description\":{\"25\":{}}}],[\"docs/{docid}/download/table-schema\",{\"_index\":58,\"title\":{},\"description\":{\"26\":{}}}],[\"docs/{docid}/download/xlsx\",{\"_index\":49,\"title\":{},\"description\":{\"24\":{}}}],[\"docs/{docid}/mov\",{\"_index\":43,\"title\":{},\"description\":{\"20\":{}}}],[\"docs/{docid}/sql\",{\"_index\":116,\"title\":{},\"description\":{\"59\":{},\"60\":{}}}],[\"docs/{docid}/t\",{\"_index\":66,\"title\":{},\"description\":{\"33\":{},\"34\":{},\"35\":{}}}],[\"docs/{docid}/tables/{tableid}/column\",{\"_index\":68,\"title\":{},\"description\":{\"37\":{},\"38\":{},\"39\":{},\"40\":{}}}],[\"docs/{docid}/tables/{tableid}/columns/{colid\",{\"_index\":69,\"title\":{},\"description\":{\"41\":{}}}],[\"docs/{docid}/tables/{tableid}/data\",{\"_index\":89,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"docs/{docid}/tables/{tableid}/data/delet\",{\"_index\":90,\"title\":{},\"description\":{\"46\":{}}}],[\"docs/{docid}/tables/{tableid}/record\",{\"_index\":62,\"title\":{},\"description\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{}}}],[\"docs/{docid}/webhook\",{\"_index\":105,\"title\":{},\"description\":{\"53\":{},\"54\":{}}}],[\"docs/{docid}/webhooks/queu\",{\"_index\":111,\"title\":{},\"description\":{\"57\":{}}}],[\"docs/{docid}/webhooks/{webhookid\",{\"_index\":106,\"title\":{},\"description\":{\"55\":{},\"56\":{}}}],[\"document\",{\"_index\":24,\"title\":{\"8\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"33\":{},\"34\":{},\"35\":{},\"53\":{},\"54\":{},\"59\":{},\"60\":{}},\"description\":{\"7\":{},\"15\":{},\"32\":{},\"47\":{},\"52\":{}}}],[\"document'\",{\"_index\":107,\"title\":{\"57\":{}},\"description\":{}}],[\"download\",{\"_index\":98,\"title\":{\"51\":{}},\"description\":{}}],[\"empti\",{\"_index\":28,\"title\":{\"9\":{},\"16\":{},\"57\":{}},\"description\":{}}],[\"endpoint\",{\"_index\":78,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"enumer\",{\"_index\":12,\"title\":{},\"description\":{\"2\":{}}}],[\"excel\",{\"_index\":48,\"title\":{\"24\":{}},\"description\":{}}],[\"favor\",{\"_index\":79,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"fetch\",{\"_index\":61,\"title\":{\"28\":{},\"43\":{}},\"description\":{}}],[\"file\",{\"_index\":46,\"title\":{\"23\":{},\"24\":{},\"25\":{}},\"description\":{\"47\":{}}}],[\"follow\",{\"_index\":54,\"title\":{},\"description\":{\"26\":{}}}],[\"format\",{\"_index\":76,\"title\":{},\"description\":{\"42\":{}}}],[\"frictionlessdata'\",{\"_index\":55,\"title\":{},\"description\":{\"26\":{}}}],[\"grist\",{\"_index\":35,\"title\":{},\"description\":{\"15\":{}}}],[\"group\",{\"_index\":23,\"title\":{},\"description\":{\"7\":{}}}],[\"immedi\",{\"_index\":80,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"includ\",{\"_index\":92,\"title\":{},\"description\":{\"47\":{}}}],[\"list\",{\"_index\":10,\"title\":{\"2\":{},\"5\":{},\"8\":{},\"13\":{},\"21\":{},\"33\":{},\"37\":{},\"48\":{}},\"description\":{}}],[\"metadata\",{\"_index\":38,\"title\":{\"18\":{},\"48\":{},\"50\":{}},\"description\":{}}],[\"modifi\",{\"_index\":17,\"title\":{\"4\":{},\"11\":{},\"18\":{},\"30\":{},\"35\":{},\"39\":{},\"45\":{},\"55\":{}},\"description\":{}}],[\"move\",{\"_index\":40,\"title\":{\"20\":{}},\"description\":{}}],[\"new\",{\"_index\":87,\"title\":{\"54\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"now\",{\"_index\":73,\"title\":{},\"description\":{\"42\":{}}}],[\"option\",{\"_index\":117,\"title\":{\"60\":{}},\"description\":{}}],[\"org\",{\"_index\":3,\"title\":{\"1\":{},\"2\":{},\"3\":{},\"4\":{},\"5\":{},\"6\":{},\"8\":{},\"20\":{}},\"description\":{\"1\":{},\"2\":{}}}],[\"organ\",{\"_index\":22,\"title\":{},\"description\":{\"7\":{}}}],[\"orgs/{orgid\",{\"_index\":16,\"title\":{},\"description\":{\"3\":{},\"4\":{}}}],[\"orgs/{orgid}/access\",{\"_index\":19,\"title\":{},\"description\":{\"5\":{},\"6\":{}}}],[\"orgs/{orgid}/workspac\",{\"_index\":26,\"title\":{},\"description\":{\"8\":{},\"9\":{}}}],[\"paramet\",{\"_index\":118,\"title\":{\"60\":{}},\"description\":{}}],[\"payload\",{\"_index\":110,\"title\":{\"57\":{}},\"description\":{}}],[\"person\",{\"_index\":6,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"plan\",{\"_index\":81,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"point\",{\"_index\":86,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"project\",{\"_index\":88,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"queri\",{\"_index\":114,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"queue\",{\"_index\":108,\"title\":{\"57\":{}},\"description\":{}}],[\"recommend\",{\"_index\":77,\"title\":{},\"description\":{\"42\":{}}}],[\"record\",{\"_index\":59,\"title\":{\"27\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{}},\"description\":{\"27\":{},\"42\":{},\"43\":{},\"44\":{},\"45\":{},\"47\":{}}}],[\"refer\",{\"_index\":93,\"title\":{},\"description\":{\"47\":{}}}],[\"remov\",{\"_index\":82,\"title\":{\"56\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"request\",{\"_index\":102,\"title\":{},\"description\":{\"52\":{}}}],[\"row\",{\"_index\":60,\"title\":{\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{}}}],[\"run\",{\"_index\":113,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"same\",{\"_index\":42,\"title\":{\"20\":{}},\"description\":{}}],[\"schema\",{\"_index\":53,\"title\":{\"26\":{}},\"description\":{\"26\":{}}}],[\"securitydefinit\",{\"_index\":1,\"title\":{},\"description\":{\"0\":{}}}],[\"site\",{\"_index\":5,\"title\":{},\"description\":{\"1\":{},\"2\":{},\"7\":{}}}],[\"space\",{\"_index\":7,\"title\":{},\"description\":{\"1\":{}}}],[\"sql\",{\"_index\":112,\"title\":{\"58\":{},\"59\":{},\"60\":{}},\"description\":{}}],[\"sqlite\",{\"_index\":45,\"title\":{\"23\":{}},\"description\":{}}],[\"standard](https://specs.frictionlessdata.io/table-schema\",{\"_index\":57,\"title\":{},\"description\":{\"26\":{}}}],[\"start\",{\"_index\":85,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"structur\",{\"_index\":65,\"title\":{},\"description\":{\"32\":{},\"36\":{}}}],[\"tabl\",{\"_index\":50,\"title\":{\"25\":{},\"26\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{},\"43\":{},\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{},\"32\":{},\"36\":{},\"42\":{}}}],[\"table-schema\",{\"_index\":56,\"title\":{},\"description\":{\"26\":{}}}],[\"team\",{\"_index\":4,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"trigger\",{\"_index\":101,\"title\":{},\"description\":{\"52\":{}}}],[\"type\",{\"_index\":94,\"title\":{},\"description\":{\"47\":{}}}],[\"undeliv\",{\"_index\":109,\"title\":{\"57\":{}},\"description\":{}}],[\"updat\",{\"_index\":64,\"title\":{\"31\":{},\"40\":{}},\"description\":{}}],[\"upload\",{\"_index\":96,\"title\":{\"49\":{}},\"description\":{}}],[\"url\",{\"_index\":103,\"title\":{},\"description\":{\"52\":{}}}],[\"us\",{\"_index\":72,\"title\":{},\"description\":{\"42\":{},\"47\":{}}}],[\"user\",{\"_index\":18,\"title\":{\"5\":{},\"13\":{},\"21\":{}},\"description\":{}}],[\"webhook\",{\"_index\":100,\"title\":{\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{}},\"description\":{\"52\":{}}}],[\"within\",{\"_index\":25,\"title\":{\"8\":{}},\"description\":{}}],[\"work\",{\"_index\":71,\"title\":{},\"description\":{\"42\":{}}}],[\"workspac\",{\"_index\":21,\"title\":{\"7\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{},\"12\":{},\"13\":{},\"14\":{},\"20\":{}},\"description\":{\"7\":{},\"15\":{}}}],[\"workspaces/{workspaceid\",{\"_index\":29,\"title\":{},\"description\":{\"10\":{},\"11\":{},\"12\":{}}}],[\"workspaces/{workspaceid}/access\",{\"_index\":31,\"title\":{},\"description\":{\"13\":{},\"14\":{}}}],[\"workspaces/{workspaceid}/doc\",{\"_index\":36,\"title\":{},\"description\":{\"16\":{}}}]],\"pipeline\":[]}},\"options\":{\"theme\":{\"spacing\":{\"sectionVertical\":2},\"breakpoints\":{\"medium\":\"50rem\",\"large\":\"50rem\"},\"sidebar\":{\"width\":\"0px\"}},\"hideDownloadButton\":true,\"pathInMiddlePanel\":true,\"scrollYOffset\":48,\"jsonSampleExpandLevel\":\"all\"}}; var container = document.getElementById('redoc'); Redoc.hydrate(__redoc_state, container);","title":"REST API reference"},{"location":"api/#grist-api-reference","text":"REST API for manipulating documents, workspaces, and team sites. API Usage is an introduction to using the API. API Console allows you to make API calls from the browser. Authentication orgs get List the orgs you have access to get Describe an org patch Modify an org get List users with access to org patch Change who has access to org workspaces get List workspaces and documents within an org post Create an empty workspace get Describe a workspace patch Modify a workspace del Delete a workspace get List users with access to workspace patch Change who has access to workspace docs post Create an empty document get Describe a document patch Modify document metadata (but not its contents) del Delete a document patch Move document to another workspace in the same org. get List users with access to document patch Change who has access to document get Content of document, as an Sqlite file get Content of document, as an Excel file get Content of table, as a CSV file get The schema of a table records get Fetch records from a table post Add records to a table patch Modify records of a table put Add or update records of a table tables get List tables in a document post Add tables to a document patch Modify tables of a document columns get List columns in a table post Add columns to a table patch Modify columns of a table put Add or update columns of a table del Delete a column of a table data get Fetch data from a table post Add rows to a table patch Modify rows of a table post Delete rows of a table attachments get List metadata of all attachments in a doc post Upload attachments to a doc get Get the metadata for an attachment get Download the contents of an attachment webhooks get Webhooks associated with a document post Create new webhooks for a document patch Modify a webhook del Remove a webhook del Empty a document's queue of undelivered payloads sql get Run an SQL query against a document post Run an SQL query against a document, with options or parameters API docs by Redocly","title":"Grist API Reference"},{"location":"integrators/","text":"Integrator Services # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make Configuring Integrators # Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables. Example: Storing form submissions # Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in! Example: Sending email alerts # We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win! Readiness column # Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example . Triggering (or avoiding triggering) on pre-existing records # The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Integrator Services"},{"location":"integrators/#integrator-services","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make","title":"Integrator Services"},{"location":"integrators/#configuring-integrators","text":"Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables.","title":"Configuring Integrators"},{"location":"integrators/#example-storing-form-submissions","text":"Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in!","title":"Example: Storing form submissions"},{"location":"integrators/#example-sending-email-alerts","text":"We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win!","title":"Example: Sending email alerts"},{"location":"integrators/#readiness-column","text":"Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example .","title":"Readiness column"},{"location":"integrators/#triggering-or-avoiding-triggering-on-pre-existing-records","text":"The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Triggering (or avoiding triggering) on pre-existing records"},{"location":"embedding/","text":"Embedding Grist # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view: Parameters # Read-Only vs. Editable # Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing . Appearance # Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Embedding"},{"location":"embedding/#embedding-grist","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view:","title":"Embedding Grist"},{"location":"embedding/#parameters","text":"","title":"Parameters"},{"location":"embedding/#read-only-vs-editable","text":"Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing .","title":"Read-Only vs. Editable"},{"location":"embedding/#appearance","text":"Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Appearance"},{"location":"code/","text":"Plugin API # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Intro to Plugin API"},{"location":"code/#plugin-api","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Plugin API"},{"location":"code/modules/grist_plugin_api/","text":"Module: grist-plugin-api # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Table of contents # Interfaces # AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap Type Aliases # ColumnsToMap UIRowId Variables # checkers docApi sectionApi selectedTable viewApi widgetApi Functions # allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows Type Aliases # ColumnsToMap # \u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget. UIRowId # \u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row. Variables # checkers # \u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above. docApi # \u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default. sectionApi # \u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget. selectedTable # \u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets). viewApi # \u2022 Const viewApi : GristView Interface for the records backing a custom widget. widgetApi # \u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget. Functions # allowSelectBy # \u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message. Returns # Promise < void > clearOptions # \u25b8 clearOptions (): Promise < void > Clears all the options. Returns # Promise < void > fetchSelectedRecord # \u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default. Parameters # Name Type rowId number options FetchSelectedOptions Returns # Promise < any > fetchSelectedTable # \u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default. Parameters # Name Type options FetchSelectedOptions Returns # Promise < any > getAccessToken # \u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); }); Parameters # Name Type options? AccessTokenOptions Returns # Promise < AccessTokenResult > getOption # \u25b8 getOption ( key ): Promise < any > Get single value from Widget options object. Parameters # Name Type key string Returns # Promise < any > getOptions # \u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object. Returns # Promise < null | object > getTable # \u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted. Parameters # Name Type tableId? string Returns # TableOperations mapColumnNames # \u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean Returns # any mapColumnNamesBack # \u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap Returns # any on # \u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101 Parameters # Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function Returns # Rpc onNewRecord # \u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected. Parameters # Name Type callback ( mappings : null | WidgetColumnMap ) => unknown Returns # void onOptions # \u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it. Parameters # Name Type callback ( options : any , settings : InteractionOptions ) => unknown Returns # void onRecord # \u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false . Parameters # Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void onRecords # \u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false . Parameters # Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void ready # \u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called. Parameters # Name Type settings? ReadyPayload Returns # void setCursorPos # \u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking. Parameters # Name Type pos CursorPos Returns # Promise < void > setOption # \u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary). Parameters # Name Type key string value any Returns # Promise < void > setOptions # \u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget. Parameters # Name Type options Object Returns # Promise < void > setSelectedRows # \u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget. Parameters # Name Type rowIds null | number [] Returns # Promise < void >","title":"grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#module-grist-plugin-api","text":"Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing .","title":"Module: grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/grist_plugin_api/#interfaces","text":"AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap","title":"Interfaces"},{"location":"code/modules/grist_plugin_api/#type-aliases","text":"ColumnsToMap UIRowId","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#variables","text":"checkers docApi sectionApi selectedTable viewApi widgetApi","title":"Variables"},{"location":"code/modules/grist_plugin_api/#functions","text":"allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows","title":"Functions"},{"location":"code/modules/grist_plugin_api/#type-aliases_1","text":"","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#columnstomap","text":"\u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget.","title":"ColumnsToMap"},{"location":"code/modules/grist_plugin_api/#uirowid","text":"\u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row.","title":"UIRowId"},{"location":"code/modules/grist_plugin_api/#variables_1","text":"","title":"Variables"},{"location":"code/modules/grist_plugin_api/#checkers","text":"\u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above.","title":"checkers"},{"location":"code/modules/grist_plugin_api/#docapi","text":"\u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default.","title":"docApi"},{"location":"code/modules/grist_plugin_api/#sectionapi","text":"\u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget.","title":"sectionApi"},{"location":"code/modules/grist_plugin_api/#selectedtable","text":"\u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets).","title":"selectedTable"},{"location":"code/modules/grist_plugin_api/#viewapi","text":"\u2022 Const viewApi : GristView Interface for the records backing a custom widget.","title":"viewApi"},{"location":"code/modules/grist_plugin_api/#widgetapi","text":"\u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget.","title":"widgetApi"},{"location":"code/modules/grist_plugin_api/#functions_1","text":"","title":"Functions"},{"location":"code/modules/grist_plugin_api/#allowselectby","text":"\u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message.","title":"allowSelectBy"},{"location":"code/modules/grist_plugin_api/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#clearoptions","text":"\u25b8 clearOptions (): Promise < void > Clears all the options.","title":"clearOptions"},{"location":"code/modules/grist_plugin_api/#returns_1","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedrecord","text":"\u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default.","title":"fetchSelectedRecord"},{"location":"code/modules/grist_plugin_api/#parameters","text":"Name Type rowId number options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_2","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedtable","text":"\u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default.","title":"fetchSelectedTable"},{"location":"code/modules/grist_plugin_api/#parameters_1","text":"Name Type options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_3","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getaccesstoken","text":"\u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); });","title":"getAccessToken"},{"location":"code/modules/grist_plugin_api/#parameters_2","text":"Name Type options? AccessTokenOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_4","text":"Promise < AccessTokenResult >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoption","text":"\u25b8 getOption ( key ): Promise < any > Get single value from Widget options object.","title":"getOption"},{"location":"code/modules/grist_plugin_api/#parameters_3","text":"Name Type key string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_5","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoptions","text":"\u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object.","title":"getOptions"},{"location":"code/modules/grist_plugin_api/#returns_6","text":"Promise < null | object >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#gettable","text":"\u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted.","title":"getTable"},{"location":"code/modules/grist_plugin_api/#parameters_4","text":"Name Type tableId? string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_7","text":"TableOperations","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnames","text":"\u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping.","title":"mapColumnNames"},{"location":"code/modules/grist_plugin_api/#parameters_5","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_8","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnamesback","text":"\u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically.","title":"mapColumnNamesBack"},{"location":"code/modules/grist_plugin_api/#parameters_6","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_9","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#on","text":"\u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101","title":"on"},{"location":"code/modules/grist_plugin_api/#parameters_7","text":"Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_10","text":"Rpc","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onnewrecord","text":"\u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected.","title":"onNewRecord"},{"location":"code/modules/grist_plugin_api/#parameters_8","text":"Name Type callback ( mappings : null | WidgetColumnMap ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_11","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onoptions","text":"\u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it.","title":"onOptions"},{"location":"code/modules/grist_plugin_api/#parameters_9","text":"Name Type callback ( options : any , settings : InteractionOptions ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_12","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecord","text":"\u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false .","title":"onRecord"},{"location":"code/modules/grist_plugin_api/#parameters_10","text":"Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_13","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecords","text":"\u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false .","title":"onRecords"},{"location":"code/modules/grist_plugin_api/#parameters_11","text":"Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_14","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#ready","text":"\u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called.","title":"ready"},{"location":"code/modules/grist_plugin_api/#parameters_12","text":"Name Type settings? ReadyPayload","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_15","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setcursorpos","text":"\u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking.","title":"setCursorPos"},{"location":"code/modules/grist_plugin_api/#parameters_13","text":"Name Type pos CursorPos","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_16","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoption","text":"\u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary).","title":"setOption"},{"location":"code/modules/grist_plugin_api/#parameters_14","text":"Name Type key string value any","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_17","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoptions","text":"\u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget.","title":"setOptions"},{"location":"code/modules/grist_plugin_api/#parameters_15","text":"Name Type options Object","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_18","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setselectedrows","text":"\u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget.","title":"setSelectedRows"},{"location":"code/modules/grist_plugin_api/#parameters_16","text":"Name Type rowIds null | number []","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_19","text":"Promise < void >","title":"Returns"},{"location":"self-managed/","text":"Self-Managed Grist # Warning The current page still doesn\u2019t have a translation for this language. But you can help translating it: Contributing . Self-Managed Grist The essentials What is Self-Managed Grist? How do I install Grist? Grist on AWS How do I sandbox documents? XSAVE not available PTRACE not available How do I run Grist on a server? How do I set up a team? How do I set up authentication? Are there other authentication methods? How do I activate Grist Enterprise? Customization How do I customize styling? How do I list custom widgets? How do I set up email notifications? How do I add more python packages? Operations What are the hardware requirements for hosting Grist? What files does Grist store? What is a \u201chome\u201d database? What is a state store? How do I set up snapshots? How do I control telemetry? How do I upgrade my installation? What if I need high availability? The essentials # What is Self-Managed Grist? # There are four flavors of Grist: SaaS (Software as a Service): Grist is available as a hosted service at docs.getgrist.com . No installation needed. Free and paid plans, with usage limits. Desktop App : Grist is available as a desktop application, built with Electron. It is available for download at https://github.com/gristlabs/grist-electron/releases . This desktop application does not need internet and is not tied to any online account or service. Self-Managed Enterprise : Grist is available as a licensed application installed by enterprises on their own infrastructure with our support and backing. Contains proprietary features developed for enterprises with particular needs. Self-Managed Core : Grist is available as a free application installed by citizen developers on their own infrastructure with community support. Grist documents created with our SaaS and Enterprise offerings can be opened and edited with Core, and vice versa. This establishes Grist documents as a reliable format for archiving and interchange. Self-Managed Grist, be it Enterprise or Core, is installed and configured in much the same way, as described in the following sections. For clarity, the sections are tagged with which flavor they apply to, for example: The full source code for Grist Core is always available at github.com/gristlabs/grist-core and is under an Apache-2.0 license. You may use and redistribute Core freely, under the terms of the free software license. The full source for Grist Enterprise is also available, at github.com/gristlabs/grist-ee , under a proprietary license that does not grant any automatic rights to use or redistribute the software. You can evaluate Enterprise for 30 days using the instructions in the following sections, or sign up for our Grist Enterprise plan and get support. How do I install Grist? # The easiest way to install Grist is as a container. We will describe how using Docker , but there are many other tools and services for running containers. To try Grist out using Docker, make an empty directory for Grist to store material in (say ~/grist ) and then for Grist Core you can do: docker run -p 8484:8484 \\ -v ~/grist:/persist \\ -e GRIST_SESSION_SECRET=invent-a-secret-here \\ -it gristlabs/grist For Grist Enterprise use gristlabs/grist-ee instead of gristlabs/grist . You should then be able to visit http://localhost:8484 in your browser. Already you will be able to create and edit Grist documents, and to open and edit documents downloaded from another Grist installation (such as our SaaS). If using some other tool or service, here are the important points: The container name is gristlabs/grist or gristlabs/grist-ee (for some tools, you may need to prefix these names with docker.io/ ). A volume (or mount, or directory) needs to be available at location /persist within the container. It can be initially empty - Grist will populate it. Without this volume, nothing you do will be stored long-term. Port 8484 on the container needs to be exposed. This can be changed if you also set the PORT environment variable for the container. The environment variable GRIST_SESSION_SECRET should be set to something secret for the container. Installed this way, Grist is accessible only to you. Typically you want to take at least the following steps: Set up sandboxing - this is important to place bounds on what formulas can do. Serve from a public host so you can collaborate live with others. Enable an authentication method so users can log in. Often you\u2019ll want to hook Grist up to an \u201cSSO\u201d (Single Sign-On) service you already use. We support some very general authentication methods that cover many cases, and a special authentication method for custom cases. Consider enabling snapshot support if you want Grist to handle document backups. Grist on AWS # You can also host Grist on AWS. Full instructions on this hosting method are available on the Grist AWS Marketplace page . How do I sandbox documents? # Grist allows for very powerful formulas, using Python. We recommend setting the environment variable GRIST_SANDBOX_FLAVOR to gvisor if your hardware supports it (most will), to run formulas in each document within a sandbox isolated from other documents and isolated from the network. docker run ... -e GRIST_SANDBOX_FLAVOR=gvisor \\ ... To sanity-check that formulas are being evaluated within a sandbox, you can create a document and then check that this formula gives an empty result: import glob glob.glob('/etc/*') Here are some reasons why gvisor sandboxing, as configured for Grist, may fail, and what you can do to diagnose the problem. XSAVE not available # Your processor may not be supported. On x86_64 , Sandy Bridge or later is needed. Check that the XSAVE processor flag is set. Here\u2019s a quick way to test that: grep -q '\\bxsave\\b' /proc/cpuinfo && echo \"XSAVE enabled\" || echo \"XSAVE missing\" PTRACE not available # The SYS_PTRACE capability may not be available. If running in docker, you could try explicitly granting it, if you are comfortable with making it available: docker run ... --cap-add=SYS_PTRACE ... In some cloud environments such as AWS ECS, you may need to explicitly list this capability in your container configuration. How do I run Grist on a server? # We suggest that you become familiar with all the other aspects of self-management on this page before serving Grist from a public host (especially Sandboxing ). When you do, it is important to tell Grist where it will be served from, using the APP_HOME_URL variable. For example, if you will be serving from https://grist.example.com , let Grist know like this: docker run ... -e APP_HOME_URL=\"https://grist.example.com\" \\ ... You will need to place a \u201creverse proxy\u201d in front of Grist to handle \u201cSSL termination\u201d (decrypting encypted traffic) using a certificate that establishes ownership of the site. If you don\u2019t know what this means, you could try using the Grist Omnibus which packages Grist with a reverse proxy that will use Let\u2019s Encrypt to get a certificate for you automatically. An important job of such a proxy is to correctly forward websocket connections. This amounts to two requirements: Ensure that the proxy is using HTTP 1.1 Pass the necessary Upgrade, Connection, and Host HTTP headers so that an HTTP connection can be upgraded to a websocket connection. For example, here is a minimal configuration for nginx , a possible choice for reverse proxy. server { server_name grist.example.com; location / { proxy_pass http://localhost:8484; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # WebSocket support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } } This configuration will handle basic HTTP traffic and websockets. It still requires additional SSL/TLS configuration. A simple option for self-hosting on a small scale is to use certbot by the EFF . How do I set up a team? # Grist has a concept of \u201cteam sites\u201d that are independently managed and named areas containing their own workspaces and documents. Team sites can have distinct subdomains (as on our SaaS\u2019s hosted team sites ), or be distinguished by a special path prefix. This often does not make sense for self-managed installations, where there is a single team. With a single domain and a single team, the special path prefix (which looks like /o/On all plans, the number of documents is not limited.
To prevent accidental abuse of the system by automation -tools, team sites may be limited to 1000 documents. If you encounter such a limit for legitimate +tools, team sites may be limited to 1,000 documents. If you encounter such a limit for legitimate use, please contact support to increase it.
-Older free plans had a limit of ten documents. Learn more about legacy limits.
+Older free plans had a limit of ten documents. Learn more about legacy limits.
For team sites on all plans, there is no limit on the number of team members that may be added to the site. For paid plans, the number of team members determines the price. See our pricing page for details.
Team members added to your team site may inherit access to workspaces or documents @@ -1101,18 +1106,18 @@
On the Free plan, documents have a limit of 5,000 rows.
-On the Pro plan, documents may have up to 100,000 rows. This is a rule of thumb. The actual limit depends -also on the number of tables, columns, and the average size of data in each cell. One way to -estimate it is to measure the size of the data when it is in CSV format: the limit is around 20MB -in this format. For example, a document with 200,000 rows and 12 numeric columns would reach that.
+On the Free plan, documents have a limit of 5,000 rows. The limit for Pro and Business plans is 100,000 and 150,000 rows, respectively.
+Documents are also subject to data size limits, as described below.
+There is a hard limit to a document’s total data size, determined as the row limit multiplied by 2KB. This means that documents on the Free plan have a data size limit of 10MB, with Pro and Business plan documents having limits of 200MB and 300MB, respectively. This value corresponds approximately to the size of the data in CSV format. You can see a document’s current data size on the ‘Raw Data’ page.
+For memory and performance reasons, there’s a recommended data size limit of 20MB. Documents beyond 20MB may slow down or run into memory limits depending on their complexity. As an example, a document with 100,000 rows and 24 numeric columns would reach this recommended limit. To help optimize formulas on large documents, you can use the built-in formula timer.
Attachments are counted separately. Attachments plus data in a single document are limited to 1GB on all plans.
-Uploads are limited to 50MB, both for attachments and to import data. Note that an import within this limit may result in a document that exceeds the document size limit, in which case the upload is still likely to fail.
Free plans are limited to 5,000 API calls per document per day. Pro plans raise the limit to 40,000 calls per document per day.
+Free plans are limited to 5,000 API calls per document per day. Pro and Business plans raise the limit to 40,000 and 60,000 calls per document per day, respectively.
Free plans may be rate limited to 5 API requests per second per document. The team plan does not impose such a rate limit.
Separately, there is a concurrency limit of 10 for all plans: if 10 authorized API requests are @@ -1135,7 +1140,7 @@
To determine if you’re on a legacy personal site, click on your site name (@your-name) in the top left. Personal sites on the legacy plan will say “Personal Site (Legacy)” in the dropdown menu.
-On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page.
+On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page.
DateTime:America/New_York
, Ref:Users
\",\"enum\":[\"Any\",\"Text\",\"Numeric\",\"Int\",\"Bool\",\"Date\",\"DateTime:$A + Table1.lookupOne(B=$B)
\"},\"isFormula\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column is a formula column. Use \\\"false\\\" for trigger formula column.\"},\"widgetOptions\":{\"type\":\"string\",\"description\":\"A JSON object with widget options, e.g.: {\\\"choices\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"alignment\\\": \\\"right\\\"}
\"},\"untieColIdFromLabel\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column label should not be used as the column identifier. Use \\\"false\\\" to use the label as the identifier.\"},\"recalcWhen\":{\"type\":\"integer\",\"description\":\"A number indicating when the column should be recalculated. [2, 3]
\"}}}]},\"GetFields\":{\"allOf\":[{\"$ref\":\"#/components/schemas/Fields\"},{\"type\":\"object\",\"properties\":{\"recalcDeps\":{\"type\":\"array\",\"items\":{\"type\":\"integer\"},\"description\":\"An array of column identifiers (colRefs) that this column depends on, prefixed with \\\"L\\\" constant. If any of these columns change, the column will be recalculated. E.g.: [\\\"L\\\", 2, 3]
\"},\"colRef\":{\"type\":\"integer\",\"description\":\"Column reference, e.g.: 2
\"}}}]},\"RowIds\":{\"type\":\"array\",\"example\":[101,102,103],\"items\":{\"type\":\"integer\"}},\"DocParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Competitive Analysis\"},\"isPinned\":{\"type\":\"boolean\",\"example\":false}}},\"WorkspaceParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Retreat Docs\"}}},\"OrgParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"ACME Unlimited\"}}},\"OrgAccessRead\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"OrgAccessWrite\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"WorkspaceAccessRead\":{\"type\":\"object\",\"required\":[\"maxInheritedRole\",\"users\"],\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"},\"parentAccess\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"WorkspaceAccessWrite\":{\"type\":\"object\",\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"DocAccessWrite\":{\"$ref\":\"#/components/schemas/WorkspaceAccessWrite\"},\"DocAccessRead\":{\"$ref\":\"#/components/schemas/WorkspaceAccessRead\"},\"AttachmentUpload\":{\"type\":\"object\",\"properties\":{\"upload\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"format\":\"binary\"}}}},\"AttachmentId\":{\"type\":\"number\",\"description\":\"An integer ID\"},\"AttachmentMetadata\":{\"type\":\"object\",\"properties\":{\"fileName\":{\"type\":\"string\",\"example\":\"logo.png\"},\"fileSize\":{\"type\":\"number\",\"example\":12345},\"timeUploaded\":{\"type\":\"string\",\"example\":\"2020-02-13T12:17:19.000Z\"}}},\"AttachmentMetadataList\":{\"type\":\"object\",\"required\":[\"records\"],\"properties\":{\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"fields\"],\"properties\":{\"id\":{\"type\":\"number\",\"example\":1},\"fields\":{\"$ref\":\"#/components/schemas/AttachmentMetadata\"}}}}}},\"SqlResultSet\":{\"type\":\"object\",\"required\":[\"statement\",\"records\"],\"properties\":{\"statement\":{\"type\":\"string\",\"description\":\"A copy of the SQL statement.\",\"example\":\"select * from Pets ...\"},\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"object\"}}},\"example\":[{\"fields\":{\"id\":1,\"pet\":\"cat\",\"popularity\":67}},{\"fields\":{\"id\":2,\"pet\":\"dog\",\"popularity\":95}}]}}},\"TableSchemaResult\":{\"type\":\"object\",\"required\":[\"name\",\"title\",\"schema\"],\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"The ID (technical name) of the table\"},\"title\":{\"type\":\"string\",\"description\":\"The human readable name of the table\"},\"path\":{\"type\":\"string\",\"description\":\"The URL to download the CSV\",\"example\":\"https://getgrist.com/o/docs/api/docs/ID/download/csv?tableId=Table1&....\"},\"format\":{\"type\":\"string\",\"enum\":[\"csv\"]},\"mediatype\":{\"type\":\"string\",\"enum\":[\"text/csv\"]},\"encoding\":{\"type\":\"string\",\"enum\":[\"utf-8\"]},\"dialect\":{\"$ref\":\"#/components/schemas/csv-dialect\"},\"schema\":{\"$ref\":\"#/components/schemas/table-schema\"}}},\"csv-dialect\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"CSV Dialect\",\"description\":\"The CSV dialect descriptor.\",\"type\":[\"string\",\"object\"],\"required\":[\"delimiter\",\"doubleQuote\"],\"properties\":{\"csvddfVersion\":{\"title\":\"CSV Dialect schema version\",\"description\":\"A number to indicate the schema version of CSV Dialect. Version 1.0 was named CSV Dialect Description Format and used different field names.\",\"type\":\"number\",\"default\":1.2,\"examples:\":[\"{\\n \\\"csvddfVersion\\\": \\\"1.2\\\"\\n}\\n\"]},\"delimiter\":{\"title\":\"Delimiter\",\"description\":\"A character sequence to use as the field separator.\",\"type\":\"string\",\"default\":\",\",\"examples\":[\"{\\n \\\"delimiter\\\": \\\",\\\"\\n}\\n\",\"{\\n \\\"delimiter\\\": \\\";\\\"\\n}\\n\"]},\"doubleQuote\":{\"title\":\"Double Quote\",\"description\":\"Specifies the handling of quotes inside fields.\",\"context\":\"If Double Quote is set to true, two consecutive quotes must be interpreted as one.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"doubleQuote\\\": true\\n}\\n\"]},\"lineTerminator\":{\"title\":\"Line Terminator\",\"description\":\"Specifies the character sequence that must be used to terminate rows.\",\"type\":\"string\",\"default\":\"\\r\\n\",\"examples\":[\"{\\n \\\"lineTerminator\\\": \\\"\\\\r\\\\n\\\"\\n}\\n\",\"{\\n \\\"lineTerminator\\\": \\\"\\\\n\\\"\\n}\\n\"]},\"nullSequence\":{\"title\":\"Null Sequence\",\"description\":\"Specifies the null sequence, for example, `\\\\N`.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"nullSequence\\\": \\\"\\\\N\\\"\\n}\\n\"]},\"quoteChar\":{\"title\":\"Quote Character\",\"description\":\"Specifies a one-character string to use as the quoting character.\",\"type\":\"string\",\"default\":\"\\\"\",\"examples\":[\"{\\n \\\"quoteChar\\\": \\\"'\\\"\\n}\\n\"]},\"escapeChar\":{\"title\":\"Escape Character\",\"description\":\"Specifies a one-character string to use as the escape character.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"escapeChar\\\": \\\"\\\\\\\\\\\"\\n}\\n\"]},\"skipInitialSpace\":{\"title\":\"Skip Initial Space\",\"description\":\"Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"skipInitialSpace\\\": true\\n}\\n\"]},\"header\":{\"title\":\"Header\",\"description\":\"Specifies if the file includes a header row, always as the first row in the file.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"header\\\": true\\n}\\n\"]},\"commentChar\":{\"title\":\"Comment Character\",\"description\":\"Specifies that any row beginning with this one-character string, without preceeding whitespace, causes the entire line to be ignored.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"commentChar\\\": \\\"#\\\"\\n}\\n\"]},\"caseSensitiveHeader\":{\"title\":\"Case Sensitive Header\",\"description\":\"Specifies if the case of headers is meaningful.\",\"context\":\"Use of case in source CSV files is not always an intentional decision. For example, should \\\"CAT\\\" and \\\"Cat\\\" be considered to have the same meaning.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"caseSensitiveHeader\\\": true\\n}\\n\"]}},\"examples\":[\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\";\\\"\\n }\\n}\\n\",\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\"\\\\t\\\",\\n \\\"quoteChar\\\": \\\"'\\\",\\n \\\"commentChar\\\": \\\"#\\\"\\n }\\n}\\n\"]},\"table-schema\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Table Schema\",\"description\":\"A Table Schema for this resource, compliant with the [Table Schema](./tableschema/) specification.\",\"type\":[\"string\",\"object\"],\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Field\",\"type\":\"object\",\"oneOf\":[{\"type\":\"object\",\"title\":\"String Field\",\"description\":\"The field contains strings, that is, sequences of characters.\",\"required\":[\"name\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `string`.\",\"enum\":[\"string\"]},\"format\":{\"description\":\"The format keyword options for `string` are `default`, `email`, `uri`, `binary`, and `uuid`.\",\"context\":\"The following `format` options are supported:\\n * **default**: any valid string.\\n * **email**: A valid email address.\\n * **uri**: A valid URI.\\n * **binary**: A base64 encoded string representing binary data.\\n * **uuid**: A string that is a uuid.\",\"enum\":[\"default\",\"email\",\"uri\",\"binary\",\"uuid\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `string` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"pattern\":{\"type\":\"string\",\"description\":\"A regular expression pattern to test each value of the property against, where a truthy response indicates validity.\",\"context\":\"Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs).\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"constraints\\\": {\\n \\\"minLength\\\": 3,\\n \\\"maxLength\\\": 35\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Number Field\",\"description\":\"The field contains numbers of any kind including decimals.\",\"context\":\"The lexical formatting follows that of decimal in [XMLSchema](https://www.w3.org/TR/xmlschema-2/#decimal): a non-empty finite-length sequence of decimal digits separated by a period as a decimal indicator. An optional leading sign is allowed. If the sign is omitted, '+' is assumed. Leading and trailing zeroes are optional. If the fractional part is zero, the period and following zero(es) can be omitted. For example: '-1.23', '12678967.543233', '+100000.00', '210'.\\n\\nThe following special string values are permitted (case does not need to be respected):\\n - NaN: not a number\\n - INF: positive infinity\\n - -INF: negative infinity\\n\\nA number `MAY` also have a trailing:\\n - exponent: this `MUST` consist of an E followed by an optional + or - sign followed by one or more decimal digits (0-9)\\n - percentage: the percentage sign: `%`. In conversion percentages should be divided by 100.\\n\\nIf both exponent and percentages are present the percentage `MUST` follow the exponent e.g. '53E10%' (equals 5.3).\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `number`.\",\"enum\":[\"number\"]},\"format\":{\"description\":\"There are no format keyword options for `number`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"decimalChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to represent a decimal point within the number. The default value is `.`.\"},\"groupChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to group digits within the number. The default value is `null`. A common value is `,` e.g. '100,000'.\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `number` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"number\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\",\\n \\\"constraints\\\": {\\n \\\"enum\\\": [ \\\"1.00\\\", \\\"1.50\\\", \\\"2.00\\\" ]\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Integer Field\",\"description\":\"The field contains integers - that is whole numbers.\",\"context\":\"Integer values are indicated in the standard way for any valid integer.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `integer`.\",\"enum\":[\"integer\"]},\"format\":{\"description\":\"There are no format keyword options for `integer`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `integer` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\",\\n \\\"constraints\\\": {\\n \\\"unique\\\": true,\\n \\\"minimum\\\": 100,\\n \\\"maximum\\\": 9999\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Field\",\"description\":\"The field contains temporal date values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `date`.\",\"enum\":[\"date\"]},\"format\":{\"description\":\"The format keyword options for `date` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string of YYYY-MM-DD.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `date` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": \\\"01-01-1900\\\"\\n }\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"format\\\": \\\"MM-DD-YYYY\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Time Field\",\"description\":\"The field contains temporal time values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `time`.\",\"enum\":[\"time\"]},\"format\":{\"description\":\"The format keyword options for `time` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for time.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `time` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\",\\n \\\"format\\\": \\\"any\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Time Field\",\"description\":\"The field contains temporal datetime values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `datetime`.\",\"enum\":[\"datetime\"]},\"format\":{\"description\":\"The format keyword options for `datetime` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for datetime.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `datetime` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\",\\n \\\"format\\\": \\\"default\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Field\",\"description\":\"A calendar year, being an integer with 4 digits. Equivalent to [gYear in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYear)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `year`.\",\"enum\":[\"year\"]},\"format\":{\"description\":\"There are no format keyword options for `year`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `year` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1970,\\n \\\"maximum\\\": 2003\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Month Field\",\"description\":\"A calendar year month, being an integer with 1 or 2 digits. Equivalent to [gYearMonth in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYearMonth)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `yearmonth`.\",\"enum\":[\"yearmonth\"]},\"format\":{\"description\":\"There are no format keyword options for `yearmonth`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `yearmonth` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1,\\n \\\"maximum\\\": 6\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Boolean Field\",\"description\":\"The field contains boolean (true/false) data.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `boolean`.\",\"enum\":[\"boolean\"]},\"format\":{\"description\":\"There are no format keyword options for `boolean`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"trueValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"true\",\"True\",\"TRUE\",\"1\"]},\"falseValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"false\",\"False\",\"FALSE\",\"0\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `boolean` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"boolean\"}}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"registered\\\",\\n \\\"type\\\": \\\"boolean\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Object Field\",\"description\":\"The field contains data which can be parsed as a valid JSON object.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `object`.\",\"enum\":[\"object\"]},\"format\":{\"description\":\"There are no format keyword options for `object`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `object` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"extra\\\"\\n \\\"type\\\": \\\"object\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoPoint Field\",\"description\":\"The field contains data describing a geographic point.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geopoint`.\",\"enum\":[\"geopoint\"]},\"format\":{\"description\":\"The format keyword options for `geopoint` are `default`,`array`, and `object`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A string of the pattern 'lon, lat', where `lon` is the longitude and `lat` is the latitude.\\n * **array**: An array of exactly two items, where each item is either a number, or a string parsable as a number, and the first item is `lon` and the second item is `lat`.\\n * **object**: A JSON object with exactly two keys, `lat` and `lon`\",\"notes\":[\"Implementations `MUST` strip all white space in the default format of `lon, lat`.\"],\"enum\":[\"default\",\"array\",\"object\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geopoint` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\",\\n \\\"format\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoJSON Field\",\"description\":\"The field contains a JSON object according to GeoJSON or TopoJSON\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geojson`.\",\"enum\":[\"geojson\"]},\"format\":{\"description\":\"The format keyword options for `geojson` are `default` and `topojson`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A geojson object as per the [GeoJSON spec](http://geojson.org/).\\n * **topojson**: A topojson object as per the [TopoJSON spec](https://github.com/topojson/topojson-specification/blob/master/README.md)\",\"enum\":[\"default\",\"topojson\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geojson` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\",\\n \\\"format\\\": \\\"topojson\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Array Field\",\"description\":\"The field contains data which can be parsed as a valid JSON array.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `array`.\",\"enum\":[\"array\"]},\"format\":{\"description\":\"There are no format keyword options for `array`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `array` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"options\\\"\\n \\\"type\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Duration Field\",\"description\":\"The field contains a duration of time.\",\"context\":\"The lexical representation for duration is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) extended format `PnYnMnDTnHnMnS`, where `nY` represents the number of years, `nM` the number of months, `nD` the number of days, 'T' is the date/time separator, `nH` the number of hours, `nM` the number of minutes and `nS` the number of seconds. The number of seconds can include decimal digits to arbitrary precision. Date and time elements including their designator may be omitted if their value is zero, and lower order elements may also be omitted for reduced precision. Here we follow the definition of [XML Schema duration datatype](http://www.w3.org/TR/xmlschema-2/#duration) directly and that definition is implicitly inlined here.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `duration`.\",\"enum\":[\"duration\"]},\"format\":{\"description\":\"There are no format keyword options for `duration`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `duration` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"period\\\"\\n \\\"type\\\": \\\"duration\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Any Field\",\"description\":\"Any value is accepted, including values that are not captured by the type/format/constraint requirements of the specification.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `any`.\",\"enum\":[\"any\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply to `any` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"notes\\\",\\n \\\"type\\\": \\\"any\\\"\\n\"]}]},\"description\":\"An `array` of Table Schema Field objects.\",\"examples\":[\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\"\\n }\\n ]\\n}\\n\",\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n },\\n {\\n \\\"name\\\": \\\"my-field-name-2\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n }\\n ]\\n}\\n\"]},\"primaryKey\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"string\"}],\"description\":\"A primary key is a field name or an array of field names, whose values `MUST` uniquely identify each row in the table.\",\"context\":\"Field name in the `primaryKey` `MUST` be unique, and `MUST` match a field name in the associated table. It is acceptable to have an array with a single value, indicating that the value of a single field is the primary key.\",\"examples\":[\"{\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n}\\n\",\"{\\n \\\"primaryKey\\\": [\\n \\\"first_name\\\",\\n \\\"last_name\\\"\\n ]\\n}\\n\"]},\"foreignKeys\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Foreign Key\",\"description\":\"Table Schema Foreign Key\",\"type\":\"object\",\"required\":[\"fields\",\"reference\"],\"oneOf\":[{\"properties\":{\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"minItems\":1,\"uniqueItems\":true,\"description\":\"Fields that make up the primary key.\"}},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"minItems\":1,\"uniqueItems\":true}}}}},{\"properties\":{\"fields\":{\"type\":\"string\",\"description\":\"Fields that make up the primary key.\"},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"string\"}}}}}]},\"examples\":[\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"the-resource\\\",\\n \\\"fields\\\": \\\"state_id\\\"\\n }\\n }\\n ]\\n}\\n\",\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"\\\",\\n \\\"fields\\\": \\\"id\\\"\\n }\\n }\\n ]\\n}\\n\"]},\"missingValues\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"default\":[\"\"],\"description\":\"Values that when encountered in the source, should be considered as `null`, 'not present', or 'blank' values.\",\"context\":\"Many datasets arrive with missing data values, either because a value was not collected or it never existed.\\nMissing values may be indicated simply by the value being empty in other cases a special value may have been used e.g. `-`, `NaN`, `0`, `-9999` etc.\\nThe `missingValues` property provides a way to indicate that these values should be interpreted as equivalent to null.\\n\\n`missingValues` are strings rather than being the data type of the particular field. This allows for comparison prior to casting and for fields to have missing value which are not of their type, for example a `number` field to have missing values indicated by `-`.\\n\\nThe default value of `missingValue` for a non-string type field is the empty string `''`. For string type fields there is no default for `missingValue` (for string fields the empty string `''` is a valid value and need not indicate null).\",\"examples\":[\"{\\n \\\"missingValues\\\": [\\n \\\"-\\\",\\n \\\"NaN\\\",\\n \\\"\\\"\\n ]\\n}\\n\",\"{\\n \\\"missingValues\\\": []\\n}\\n\"]}},\"examples\":[\"{\\n \\\"schema\\\": {\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"first_name\\\",\\n \\\"type\\\": \\\"string\\\"\\n \\\"constraints\\\": {\\n \\\"required\\\": true\\n }\\n },\\n {\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\"\\n },\\n ],\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n }\\n}\\n\"]}},\"parameters\":{\"filterQueryParam\":{\"in\":\"query\",\"name\":\"filter\",\"schema\":{\"type\":\"string\"},\"description\":\"This is a JSON object mapping column names to arrays of allowed values. For example, to filter column `pet` for values `cat` and `dog`, the filter would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}`. JSON contains characters that are not safe to place in a URL, so it is important to url-encode them. For this example, the url-encoding is `%7B%22pet%22%3A%20%5B%22cat%22%2C%20%22dog%22%5D%7D`. See https://rosettacode.org/wiki/URL_encoding for how to url-encode a string, or https://www.urlencoder.org/ to try some examples. Multiple columns can be filtered. For example the filter for `pet` being either `cat` or `dog`, AND `size` being either `tiny` or `outrageously small`, would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"size\\\": [\\\"tiny\\\", \\\"outrageously small\\\"]}`.\",\"example\":\"{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}\",\"required\":false},\"sortQueryParam\":{\"in\":\"query\",\"name\":\"sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Order in which to return results. If a single column name is given (e.g. `pet`), results are placed in ascending order of values in that column. To get results in an order that was previously prepared manually in Grist, use the special `manualSort` column name. Multiple columns can be specified, separated by commas (e.g. `pet,age`). For descending order, prefix a column name with a `-` character (e.g. `pet,-age`). To include additional sorting options append them after a colon (e.g. `pet,-age:naturalSort;emptyFirst,owner`). Available options are: `choiceOrder`, `naturalSort`, `emptyFirst`. Without the `sort` parameter, the order of results is unspecified.\",\"example\":\"pet,-age\",\"required\":false},\"limitQueryParam\":{\"in\":\"query\",\"name\":\"limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Return at most this number of rows. A value of 0 is equivalent to having no limit.\",\"example\":\"5\",\"required\":false},\"sortHeaderParam\":{\"in\":\"header\",\"name\":\"X-Sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Same as `sort` query parameter.\",\"example\":\"pet,-age\",\"required\":false},\"limitHeaderParam\":{\"in\":\"header\",\"name\":\"X-Limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Same as `limit` query parameter.\",\"example\":\"5\",\"required\":false},\"colIdPathParam\":{\"in\":\"path\",\"name\":\"colId\",\"schema\":{\"type\":\"string\"},\"description\":\"The column id (without the starting `$`) as shown in the column configuration below the label\",\"required\":true},\"tableIdPathParam\":{\"in\":\"path\",\"name\":\"tableId\",\"schema\":{\"type\":\"string\"},\"description\":\"normalized table name (see `TABLE ID` in Raw Data) or numeric row ID in `_grist_Tables`\",\"required\":true},\"docIdPathParam\":{\"in\":\"path\",\"name\":\"docId\",\"schema\":{\"type\":\"string\"},\"description\":\"A string id (UUID)\",\"required\":true},\"orgIdPathParam\":{\"in\":\"path\",\"name\":\"orgId\",\"schema\":{\"oneOf\":[{\"type\":\"integer\"},{\"type\":\"string\"}]},\"description\":\"This can be an integer id, or a string subdomain (e.g. `gristlabs`), or `current` if the org is implied by the domain in the url\",\"required\":true},\"workspaceIdPathParam\":{\"in\":\"path\",\"name\":\"workspaceId\",\"description\":\"An integer id\",\"schema\":{\"type\":\"integer\"},\"required\":true},\"noparseQueryParam\":{\"in\":\"query\",\"name\":\"noparse\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to prohibit parsing strings according to the column type.\"},\"hiddenQueryParam\":{\"in\":\"query\",\"name\":\"hidden\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to include the hidden columns (like \\\"manualSort\\\")\"},\"headerQueryParam\":{\"in\":\"query\",\"name\":\"header\",\"schema\":{\"type\":\"string\",\"enum\":[\"colId\",\"label\"]},\"description\":\"Format for headers. Labels tend to be more human-friendly while colIds are more normalized.\",\"required\":false}}}}},\"searchIndex\":{\"store\":[\"section/Authentication\",\"tag/orgs\",\"tag/orgs/operation/listOrgs\",\"tag/orgs/operation/describeOrg\",\"tag/orgs/operation/modifyOrg\",\"tag/orgs/operation/listOrgAccess\",\"tag/orgs/operation/modifyOrgAccess\",\"tag/workspaces\",\"tag/workspaces/operation/listWorkspaces\",\"tag/workspaces/operation/createWorkspace\",\"tag/workspaces/operation/describeWorkspace\",\"tag/workspaces/operation/modifyWorkspace\",\"tag/workspaces/operation/deleteWorkspace\",\"tag/workspaces/operation/listWorkspaceAccess\",\"tag/workspaces/operation/modifyWorkspaceAccess\",\"tag/docs\",\"tag/docs/operation/createDoc\",\"tag/docs/operation/describeDoc\",\"tag/docs/operation/modifyDoc\",\"tag/docs/operation/deleteDoc\",\"tag/docs/operation/moveDoc\",\"tag/docs/operation/listDocAccess\",\"tag/docs/operation/modifyDocAccess\",\"tag/docs/operation/downloadDoc\",\"tag/docs/operation/downloadDocXlsx\",\"tag/docs/operation/downloadDocCsv\",\"tag/docs/operation/downloadTableSchema\",\"tag/records\",\"tag/records/operation/listRecords\",\"tag/records/operation/addRecords\",\"tag/records/operation/modifyRecords\",\"tag/records/operation/replaceRecords\",\"tag/tables\",\"tag/tables/operation/listTables\",\"tag/tables/operation/addTables\",\"tag/tables/operation/modifyTables\",\"tag/columns\",\"tag/columns/operation/listColumns\",\"tag/columns/operation/addColumns\",\"tag/columns/operation/modifyColumns\",\"tag/columns/operation/replaceColumns\",\"tag/columns/operation/deleteColumn\",\"tag/data\",\"tag/data/operation/getTableData\",\"tag/data/operation/addRows\",\"tag/data/operation/modifyRows\",\"tag/data/operation/deleteRows\",\"tag/attachments\",\"tag/attachments/operation/listAttachments\",\"tag/attachments/operation/uploadAttachments\",\"tag/attachments/operation/getAttachmentMetadata\",\"tag/attachments/operation/downloadAttachment\",\"tag/webhooks\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/get\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/post\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/patch\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/delete\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1queue/delete\",\"tag/sql\",\"tag/sql/paths/~1docs~1{docId}~1sql/get\",\"tag/sql/paths/~1docs~1{docId}~1sql/post\"],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"title\",\"description\"],\"fieldVectors\":[[\"title/0\",[0,5.068]],[\"description/0\",[1,4.208,2,4.208]],[\"title/1\",[3,2.418]],[\"description/1\",[3,1.097,4,1.984,5,1.776,6,1.984,7,2.3,8,1.621,9,2.3]],[\"title/2\",[3,1.739,10,1.946,11,2.068]],[\"description/2\",[3,1.097,4,1.984,5,1.776,6,1.984,12,2.3,13,2.3,14,2.3]],[\"title/3\",[3,2.023,15,3.275]],[\"description/3\",[16,4.353]],[\"title/4\",[3,2.023,17,2.264]],[\"description/4\",[16,4.353]],[\"title/5\",[3,1.525,10,1.706,11,1.814,18,2.468]],[\"description/5\",[19,4.353]],[\"title/6\",[3,1.739,11,2.068,20,2.569]],[\"description/6\",[19,4.353]],[\"title/7\",[21,2.294]],[\"description/7\",[5,1.953,8,1.782,21,1.145,22,2.529,23,2.529,24,0.689]],[\"title/8\",[3,1.358,10,1.519,21,1.288,24,0.775,25,2.845]],[\"description/8\",[26,4.353]],[\"title/9\",[21,1.65,27,2.815,28,2.815]],[\"description/9\",[26,4.353]],[\"title/10\",[15,3.275,21,1.919]],[\"description/10\",[29,3.896]],[\"title/11\",[17,2.264,21,1.919]],[\"description/11\",[29,3.896]],[\"title/12\",[21,1.919,30,2.988]],[\"description/12\",[29,3.896]],[\"title/13\",[10,1.706,11,1.814,18,2.468,21,1.447]],[\"description/13\",[31,4.353]],[\"title/14\",[11,2.068,20,2.569,21,1.65]],[\"description/14\",[31,4.353]],[\"title/15\",[32,3.914]],[\"description/15\",[21,1.272,24,0.765,33,2.424,34,1.98,35,2.809]],[\"title/16\",[24,0.993,27,2.815,28,2.815]],[\"description/16\",[36,5.045]],[\"title/17\",[15,3.275,24,1.155]],[\"description/17\",[37,3.896]],[\"title/18\",[17,1.706,24,0.87,38,2.468,39,2.08]],[\"description/18\",[37,3.896]],[\"title/19\",[24,1.155,30,2.988]],[\"description/19\",[37,3.896]],[\"title/20\",[3,1.223,21,1.161,24,0.698,40,2.564,41,2.564,42,2.564]],[\"description/20\",[43,5.045]],[\"title/21\",[10,1.706,11,1.814,18,2.468,24,0.87]],[\"description/21\",[44,4.353]],[\"title/22\",[11,2.068,20,2.569,24,0.993]],[\"description/22\",[44,4.353]],[\"title/23\",[24,0.87,39,2.08,45,3.196,46,2.252]],[\"description/23\",[47,5.045]],[\"title/24\",[24,0.87,39,2.08,46,2.252,48,3.196]],[\"description/24\",[49,5.045]],[\"title/25\",[39,2.08,46,2.252,50,0.833,51,3.196]],[\"description/25\",[52,5.045]],[\"title/26\",[50,1.105,53,3.658]],[\"description/26\",[53,2.182,54,2.529,55,2.529,56,2.529,57,2.529,58,2.529]],[\"title/27\",[59,2.294]],[\"description/27\",[8,1.782,33,2.182,34,1.782,50,0.659,59,1.145,60,1.782]],[\"title/28\",[50,0.95,59,1.65,61,3.144]],[\"description/28\",[62,3.556]],[\"title/29\",[50,0.95,59,1.65,63,2.209]],[\"description/29\",[62,3.556]],[\"title/30\",[17,1.946,50,0.95,59,1.65]],[\"description/30\",[62,3.556]],[\"title/31\",[50,0.833,59,1.447,63,1.937,64,2.757]],[\"description/31\",[62,3.556]],[\"title/32\",[50,1.321]],[\"description/32\",[24,0.86,34,2.227,50,0.823,65,2.726]],[\"title/33\",[10,1.946,24,0.993,50,0.95]],[\"description/33\",[66,3.896]],[\"title/34\",[24,0.993,50,0.95,63,2.209]],[\"description/34\",[66,3.896]],[\"title/35\",[17,1.946,24,0.993,50,0.95]],[\"description/35\",[66,3.896]],[\"title/36\",[67,2.706]],[\"description/36\",[34,2.227,50,0.823,65,2.726,67,1.687]],[\"title/37\",[10,1.946,50,0.95,67,1.946]],[\"description/37\",[68,3.556]],[\"title/38\",[50,0.95,63,2.209,67,1.946]],[\"description/38\",[68,3.556]],[\"title/39\",[17,1.946,50,0.95,67,1.946]],[\"description/39\",[68,3.556]],[\"title/40\",[50,0.833,63,1.937,64,2.757,67,1.706]],[\"description/40\",[68,3.556]],[\"title/41\",[30,2.569,50,0.95,67,1.946]],[\"description/41\",[69,5.045]],[\"title/42\",[70,3.572]],[\"description/42\",[50,0.412,59,0.716,70,1.115,71,1.582,72,1.365,73,2.652,74,1.115,75,1.582,76,1.582,77,1.582,78,1.115]],[\"title/43\",[50,0.95,61,3.144,70,2.569]],[\"description/43\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/44\",[50,0.95,60,2.569,63,2.209]],[\"description/44\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/45\",[17,1.946,50,0.95,60,2.569]],[\"description/45\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/46\",[30,2.569,50,0.95,60,2.569]],[\"description/46\",[90,5.045]],[\"title/47\",[91,3.071]],[\"description/47\",[24,0.46,46,1.189,59,0.764,67,0.901,70,1.189,72,1.456,91,1.696,92,1.687,93,1.687,94,1.687]],[\"title/48\",[10,1.706,32,2.468,38,2.468,91,1.937]],[\"description/48\",[95,4.353]],[\"title/49\",[32,2.815,91,2.209,96,3.645]],[\"description/49\",[95,4.353]],[\"title/50\",[38,3.275,91,2.569]],[\"description/50\",[97,5.045]],[\"title/51\",[39,2.372,91,2.209,98,3.645]],[\"description/51\",[99,5.045]],[\"title/52\",[100,3.071]],[\"description/52\",[8,1.621,20,1.621,24,0.626,100,1.394,101,2.3,102,2.3,103,2.3]],[\"title/53\",[24,0.993,100,2.209,104,3.645]],[\"description/53\",[105,4.353]],[\"title/54\",[24,0.87,27,2.468,87,2.252,100,1.937]],[\"description/54\",[105,4.353]],[\"title/55\",[17,2.264,100,2.569]],[\"description/55\",[106,4.353]],[\"title/56\",[82,2.988,100,2.569]],[\"description/56\",[106,4.353]],[\"title/57\",[28,2.198,107,2.845,108,2.845,109,2.845,110,2.845]],[\"description/57\",[111,5.045]],[\"title/58\",[112,3.914]],[\"description/58\",[]],[\"title/59\",[24,0.775,112,2.198,113,2.455,114,2.455,115,2.455]],[\"description/59\",[116,4.353]],[\"title/60\",[24,0.636,112,1.802,113,2.013,114,2.013,115,2.013,117,2.334,118,2.334]],[\"description/60\",[116,4.353]]],\"invertedIndex\":[[\"\",{\"_index\":2,\"title\":{},\"description\":{\"0\":{}}}],[\"access\",{\"_index\":11,\"title\":{\"2\":{},\"5\":{},\"6\":{},\"13\":{},\"14\":{},\"21\":{},\"22\":{}},\"description\":{}}],[\"add\",{\"_index\":63,\"title\":{\"29\":{},\"31\":{},\"34\":{},\"38\":{},\"40\":{},\"44\":{}},\"description\":{}}],[\"against\",{\"_index\":115,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"anoth\",{\"_index\":41,\"title\":{\"20\":{}},\"description\":{}}],[\"api\",{\"_index\":9,\"title\":{},\"description\":{\"1\":{}}}],[\"area\",{\"_index\":13,\"title\":{},\"description\":{\"2\":{}}}],[\"associ\",{\"_index\":104,\"title\":{\"53\":{}},\"description\":{}}],[\"attach\",{\"_index\":91,\"title\":{\"47\":{},\"48\":{},\"49\":{},\"50\":{},\"51\":{}},\"description\":{\"47\":{}}}],[\"authent\",{\"_index\":0,\"title\":{\"0\":{}},\"description\":{}}],[\"avail\",{\"_index\":14,\"title\":{},\"description\":{\"2\":{}}}],[\"better\",{\"_index\":84,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"call\",{\"_index\":8,\"title\":{},\"description\":{\"1\":{},\"7\":{},\"27\":{},\"52\":{}}}],[\"chang\",{\"_index\":20,\"title\":{\"6\":{},\"14\":{},\"22\":{}},\"description\":{\"52\":{}}}],[\"collect\",{\"_index\":34,\"title\":{},\"description\":{\"15\":{},\"27\":{},\"32\":{},\"36\":{}}}],[\"column\",{\"_index\":67,\"title\":{\"36\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{}},\"description\":{\"36\":{},\"47\":{}}}],[\"columnar\",{\"_index\":75,\"title\":{},\"description\":{\"42\":{}}}],[\"consid\",{\"_index\":83,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"contain\",{\"_index\":33,\"title\":{},\"description\":{\"15\":{},\"27\":{}}}],[\"content\",{\"_index\":39,\"title\":{\"18\":{},\"23\":{},\"24\":{},\"25\":{},\"51\":{}},\"description\":{}}],[\"creat\",{\"_index\":27,\"title\":{\"9\":{},\"16\":{},\"54\":{}},\"description\":{}}],[\"csv\",{\"_index\":51,\"title\":{\"25\":{}},\"description\":{}}],[\"data\",{\"_index\":70,\"title\":{\"42\":{},\"43\":{}},\"description\":{\"42\":{},\"47\":{}}}],[\"delet\",{\"_index\":30,\"title\":{\"12\":{},\"19\":{},\"41\":{},\"46\":{}},\"description\":{}}],[\"deprec\",{\"_index\":74,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"describ\",{\"_index\":15,\"title\":{\"3\":{},\"10\":{},\"17\":{}},\"description\":{}}],[\"doc\",{\"_index\":32,\"title\":{\"15\":{},\"48\":{},\"49\":{}},\"description\":{}}],[\"docs/{docid\",{\"_index\":37,\"title\":{},\"description\":{\"17\":{},\"18\":{},\"19\":{}}}],[\"docs/{docid}/access\",{\"_index\":44,\"title\":{},\"description\":{\"21\":{},\"22\":{}}}],[\"docs/{docid}/attach\",{\"_index\":95,\"title\":{},\"description\":{\"48\":{},\"49\":{}}}],[\"docs/{docid}/attachments/{attachmentid\",{\"_index\":97,\"title\":{},\"description\":{\"50\":{}}}],[\"docs/{docid}/attachments/{attachmentid}/download\",{\"_index\":99,\"title\":{},\"description\":{\"51\":{}}}],[\"docs/{docid}/download\",{\"_index\":47,\"title\":{},\"description\":{\"23\":{}}}],[\"docs/{docid}/download/csv\",{\"_index\":52,\"title\":{},\"description\":{\"25\":{}}}],[\"docs/{docid}/download/table-schema\",{\"_index\":58,\"title\":{},\"description\":{\"26\":{}}}],[\"docs/{docid}/download/xlsx\",{\"_index\":49,\"title\":{},\"description\":{\"24\":{}}}],[\"docs/{docid}/mov\",{\"_index\":43,\"title\":{},\"description\":{\"20\":{}}}],[\"docs/{docid}/sql\",{\"_index\":116,\"title\":{},\"description\":{\"59\":{},\"60\":{}}}],[\"docs/{docid}/t\",{\"_index\":66,\"title\":{},\"description\":{\"33\":{},\"34\":{},\"35\":{}}}],[\"docs/{docid}/tables/{tableid}/column\",{\"_index\":68,\"title\":{},\"description\":{\"37\":{},\"38\":{},\"39\":{},\"40\":{}}}],[\"docs/{docid}/tables/{tableid}/columns/{colid\",{\"_index\":69,\"title\":{},\"description\":{\"41\":{}}}],[\"docs/{docid}/tables/{tableid}/data\",{\"_index\":89,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"docs/{docid}/tables/{tableid}/data/delet\",{\"_index\":90,\"title\":{},\"description\":{\"46\":{}}}],[\"docs/{docid}/tables/{tableid}/record\",{\"_index\":62,\"title\":{},\"description\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{}}}],[\"docs/{docid}/webhook\",{\"_index\":105,\"title\":{},\"description\":{\"53\":{},\"54\":{}}}],[\"docs/{docid}/webhooks/queu\",{\"_index\":111,\"title\":{},\"description\":{\"57\":{}}}],[\"docs/{docid}/webhooks/{webhookid\",{\"_index\":106,\"title\":{},\"description\":{\"55\":{},\"56\":{}}}],[\"document\",{\"_index\":24,\"title\":{\"8\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"33\":{},\"34\":{},\"35\":{},\"53\":{},\"54\":{},\"59\":{},\"60\":{}},\"description\":{\"7\":{},\"15\":{},\"32\":{},\"47\":{},\"52\":{}}}],[\"document'\",{\"_index\":107,\"title\":{\"57\":{}},\"description\":{}}],[\"download\",{\"_index\":98,\"title\":{\"51\":{}},\"description\":{}}],[\"empti\",{\"_index\":28,\"title\":{\"9\":{},\"16\":{},\"57\":{}},\"description\":{}}],[\"endpoint\",{\"_index\":78,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"enumer\",{\"_index\":12,\"title\":{},\"description\":{\"2\":{}}}],[\"excel\",{\"_index\":48,\"title\":{\"24\":{}},\"description\":{}}],[\"favor\",{\"_index\":79,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"fetch\",{\"_index\":61,\"title\":{\"28\":{},\"43\":{}},\"description\":{}}],[\"file\",{\"_index\":46,\"title\":{\"23\":{},\"24\":{},\"25\":{}},\"description\":{\"47\":{}}}],[\"follow\",{\"_index\":54,\"title\":{},\"description\":{\"26\":{}}}],[\"format\",{\"_index\":76,\"title\":{},\"description\":{\"42\":{}}}],[\"frictionlessdata'\",{\"_index\":55,\"title\":{},\"description\":{\"26\":{}}}],[\"grist\",{\"_index\":35,\"title\":{},\"description\":{\"15\":{}}}],[\"group\",{\"_index\":23,\"title\":{},\"description\":{\"7\":{}}}],[\"immedi\",{\"_index\":80,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"includ\",{\"_index\":92,\"title\":{},\"description\":{\"47\":{}}}],[\"list\",{\"_index\":10,\"title\":{\"2\":{},\"5\":{},\"8\":{},\"13\":{},\"21\":{},\"33\":{},\"37\":{},\"48\":{}},\"description\":{}}],[\"metadata\",{\"_index\":38,\"title\":{\"18\":{},\"48\":{},\"50\":{}},\"description\":{}}],[\"modifi\",{\"_index\":17,\"title\":{\"4\":{},\"11\":{},\"18\":{},\"30\":{},\"35\":{},\"39\":{},\"45\":{},\"55\":{}},\"description\":{}}],[\"move\",{\"_index\":40,\"title\":{\"20\":{}},\"description\":{}}],[\"new\",{\"_index\":87,\"title\":{\"54\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"now\",{\"_index\":73,\"title\":{},\"description\":{\"42\":{}}}],[\"option\",{\"_index\":117,\"title\":{\"60\":{}},\"description\":{}}],[\"org\",{\"_index\":3,\"title\":{\"1\":{},\"2\":{},\"3\":{},\"4\":{},\"5\":{},\"6\":{},\"8\":{},\"20\":{}},\"description\":{\"1\":{},\"2\":{}}}],[\"organ\",{\"_index\":22,\"title\":{},\"description\":{\"7\":{}}}],[\"orgs/{orgid\",{\"_index\":16,\"title\":{},\"description\":{\"3\":{},\"4\":{}}}],[\"orgs/{orgid}/access\",{\"_index\":19,\"title\":{},\"description\":{\"5\":{},\"6\":{}}}],[\"orgs/{orgid}/workspac\",{\"_index\":26,\"title\":{},\"description\":{\"8\":{},\"9\":{}}}],[\"paramet\",{\"_index\":118,\"title\":{\"60\":{}},\"description\":{}}],[\"payload\",{\"_index\":110,\"title\":{\"57\":{}},\"description\":{}}],[\"person\",{\"_index\":6,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"plan\",{\"_index\":81,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"point\",{\"_index\":86,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"project\",{\"_index\":88,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"queri\",{\"_index\":114,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"queue\",{\"_index\":108,\"title\":{\"57\":{}},\"description\":{}}],[\"recommend\",{\"_index\":77,\"title\":{},\"description\":{\"42\":{}}}],[\"record\",{\"_index\":59,\"title\":{\"27\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{}},\"description\":{\"27\":{},\"42\":{},\"43\":{},\"44\":{},\"45\":{},\"47\":{}}}],[\"refer\",{\"_index\":93,\"title\":{},\"description\":{\"47\":{}}}],[\"remov\",{\"_index\":82,\"title\":{\"56\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"request\",{\"_index\":102,\"title\":{},\"description\":{\"52\":{}}}],[\"row\",{\"_index\":60,\"title\":{\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{}}}],[\"run\",{\"_index\":113,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"same\",{\"_index\":42,\"title\":{\"20\":{}},\"description\":{}}],[\"schema\",{\"_index\":53,\"title\":{\"26\":{}},\"description\":{\"26\":{}}}],[\"securitydefinit\",{\"_index\":1,\"title\":{},\"description\":{\"0\":{}}}],[\"site\",{\"_index\":5,\"title\":{},\"description\":{\"1\":{},\"2\":{},\"7\":{}}}],[\"space\",{\"_index\":7,\"title\":{},\"description\":{\"1\":{}}}],[\"sql\",{\"_index\":112,\"title\":{\"58\":{},\"59\":{},\"60\":{}},\"description\":{}}],[\"sqlite\",{\"_index\":45,\"title\":{\"23\":{}},\"description\":{}}],[\"standard](https://specs.frictionlessdata.io/table-schema\",{\"_index\":57,\"title\":{},\"description\":{\"26\":{}}}],[\"start\",{\"_index\":85,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"structur\",{\"_index\":65,\"title\":{},\"description\":{\"32\":{},\"36\":{}}}],[\"tabl\",{\"_index\":50,\"title\":{\"25\":{},\"26\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{},\"43\":{},\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{},\"32\":{},\"36\":{},\"42\":{}}}],[\"table-schema\",{\"_index\":56,\"title\":{},\"description\":{\"26\":{}}}],[\"team\",{\"_index\":4,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"trigger\",{\"_index\":101,\"title\":{},\"description\":{\"52\":{}}}],[\"type\",{\"_index\":94,\"title\":{},\"description\":{\"47\":{}}}],[\"undeliv\",{\"_index\":109,\"title\":{\"57\":{}},\"description\":{}}],[\"updat\",{\"_index\":64,\"title\":{\"31\":{},\"40\":{}},\"description\":{}}],[\"upload\",{\"_index\":96,\"title\":{\"49\":{}},\"description\":{}}],[\"url\",{\"_index\":103,\"title\":{},\"description\":{\"52\":{}}}],[\"us\",{\"_index\":72,\"title\":{},\"description\":{\"42\":{},\"47\":{}}}],[\"user\",{\"_index\":18,\"title\":{\"5\":{},\"13\":{},\"21\":{}},\"description\":{}}],[\"webhook\",{\"_index\":100,\"title\":{\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{}},\"description\":{\"52\":{}}}],[\"within\",{\"_index\":25,\"title\":{\"8\":{}},\"description\":{}}],[\"work\",{\"_index\":71,\"title\":{},\"description\":{\"42\":{}}}],[\"workspac\",{\"_index\":21,\"title\":{\"7\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{},\"12\":{},\"13\":{},\"14\":{},\"20\":{}},\"description\":{\"7\":{},\"15\":{}}}],[\"workspaces/{workspaceid\",{\"_index\":29,\"title\":{},\"description\":{\"10\":{},\"11\":{},\"12\":{}}}],[\"workspaces/{workspaceid}/access\",{\"_index\":31,\"title\":{},\"description\":{\"13\":{},\"14\":{}}}],[\"workspaces/{workspaceid}/doc\",{\"_index\":36,\"title\":{},\"description\":{\"16\":{}}}]],\"pipeline\":[]}},\"options\":{\"theme\":{\"spacing\":{\"sectionVertical\":2},\"breakpoints\":{\"medium\":\"50rem\",\"large\":\"50rem\"},\"sidebar\":{\"width\":\"0px\"}},\"hideDownloadButton\":true,\"pathInMiddlePanel\":true,\"scrollYOffset\":48,\"jsonSampleExpandLevel\":\"all\"}}; var container = document.getElementById('redoc'); Redoc.hydrate(__redoc_state, container);","title":"REST API reference"},{"location":"api/#grist-api-reference","text":"REST API for manipulating documents, workspaces, and team sites. API Usage is an introduction to using the API. API Console allows you to make API calls from the browser. Authentication orgs get List the orgs you have access to get Describe an org patch Modify an org get List users with access to org patch Change who has access to org workspaces get List workspaces and documents within an org post Create an empty workspace get Describe a workspace patch Modify a workspace del Delete a workspace get List users with access to workspace patch Change who has access to workspace docs post Create an empty document get Describe a document patch Modify document metadata (but not its contents) del Delete a document patch Move document to another workspace in the same org. get List users with access to document patch Change who has access to document get Content of document, as an Sqlite file get Content of document, as an Excel file get Content of table, as a CSV file get The schema of a table records get Fetch records from a table post Add records to a table patch Modify records of a table put Add or update records of a table tables get List tables in a document post Add tables to a document patch Modify tables of a document columns get List columns in a table post Add columns to a table patch Modify columns of a table put Add or update columns of a table del Delete a column of a table data get Fetch data from a table post Add rows to a table patch Modify rows of a table post Delete rows of a table attachments get List metadata of all attachments in a doc post Upload attachments to a doc get Get the metadata for an attachment get Download the contents of an attachment webhooks get Webhooks associated with a document post Create new webhooks for a document patch Modify a webhook del Remove a webhook del Empty a document's queue of undelivered payloads sql get Run an SQL query against a document post Run an SQL query against a document, with options or parameters API docs by Redocly","title":"Grist API Reference"},{"location":"authorship/","text":"Authorship columns # Sometimes it is useful to have columns that record who created individual records, and who last updated them. Grist lets you create such columns easily. It also automatically tracks document changes in the Activity tab of Document History, but nevertheless it is convenient to have that information in tabular form available to formulas and filters, and authorship columns let you do that. A \u201cCreated By\u201d column # Suppose we want to fill a column automatically with the name of the creator of each record as they are added. As a first step, add a column called (for example) Created By . In the column options in the side panel (see Columns for a refresher), click Set trigger formula action. Set user.Name as the column\u2019s formula. There are other possibilities, such as user.Email , a unique user.UserID , and so on. The user information available is the same as that in Access rule conditions . Time information is available as well (see Timestamp columns ). But let\u2019s stick with user.Name for now. Now, to set the column whenever a record is created, make sure that Apply to new records option is checked. And that\u2019s it! Now whenever a record is created, the Created At column will be set to the name of the user creating it: An \u201cUpdated By\u201d column # If we want a column that stores who last edited a record (as opposed to its creator), the procedure is similar to that for a \u201cCreated By\u201d column , but instead of Apply to new records , select Apply on record changes . Then select Any field (assuming you want any change in a record to count as an update) and press OK . You can alternatively pick and choose which columns, when updated, will trigger the formula. Here is an example the the new column at work - when Cotton Candy v Candy Floss is updated,a user name appears for that record: It is still possible for a user to manually edit cells in the Created By and Updated By columns. If you don\u2019t want that to be allowed, use access rules to forbid it.","title":"Authorship columns"},{"location":"authorship/#authorship-columns","text":"Sometimes it is useful to have columns that record who created individual records, and who last updated them. Grist lets you create such columns easily. It also automatically tracks document changes in the Activity tab of Document History, but nevertheless it is convenient to have that information in tabular form available to formulas and filters, and authorship columns let you do that.","title":"Authorship columns"},{"location":"authorship/#a-created-by-column","text":"Suppose we want to fill a column automatically with the name of the creator of each record as they are added. As a first step, add a column called (for example) Created By . In the column options in the side panel (see Columns for a refresher), click Set trigger formula action. Set user.Name as the column\u2019s formula. There are other possibilities, such as user.Email , a unique user.UserID , and so on. The user information available is the same as that in Access rule conditions . Time information is available as well (see Timestamp columns ). But let\u2019s stick with user.Name for now. Now, to set the column whenever a record is created, make sure that Apply to new records option is checked. And that\u2019s it! Now whenever a record is created, the Created At column will be set to the name of the user creating it:","title":"A \"Created By\" column"},{"location":"authorship/#an-updated-by-column","text":"If we want a column that stores who last edited a record (as opposed to its creator), the procedure is similar to that for a \u201cCreated By\u201d column , but instead of Apply to new records , select Apply on record changes . Then select Any field (assuming you want any change in a record to count as an update) and press OK . You can alternatively pick and choose which columns, when updated, will trigger the formula. Here is an example the the new column at work - when Cotton Candy v Candy Floss is updated,a user name appears for that record: It is still possible for a user to manually edit cells in the Created By and Updated By columns. If you don\u2019t want that to be allowed, use access rules to forbid it.","title":"An \"Updated By\" column"},{"location":"automatic-backups/","text":"Automatic Backups # Grist automatically saves backups of your documents as you work on them. These backups \u2013 or snapshots \u2013 of the document can be examined at any time. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. For example, hourly snapshots are retained for about a day, but monthly snapshots are retained for more than a year. Examining Backups # To see the list of backups, click \u201cDocument History\u201d in the left panel, then click the \u201cSnapshots\u201d tab in the panel that opens on the right: You can open any of the listed snapshots to see the document as it existed at that time. Restoring an Older Version # While examining a snapshot, the \u201cShare\u201d menu has additional options; Use the \u201cReplace Current Version\u201d option to revert your document to the version you are looking at. You can also save the snapshot as a new document using the \u201cSave Copy\u201d option. Deleted Documents # When you delete a document, its history of backups is deleted with it. While the document is in Trash, you can still restore it for 30 days. If you do, you will find all the historical snapshots still there. If you choose \u201cDelete Forever\u201d for a document in Trash, or when a document in Trash is automatically purged after 30 days, the backups will be deleted forever with it.","title":"Automatic backups"},{"location":"automatic-backups/#automatic-backups","text":"Grist automatically saves backups of your documents as you work on them. These backups \u2013 or snapshots \u2013 of the document can be examined at any time. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. For example, hourly snapshots are retained for about a day, but monthly snapshots are retained for more than a year.","title":"Automatic Backups"},{"location":"automatic-backups/#examining-backups","text":"To see the list of backups, click \u201cDocument History\u201d in the left panel, then click the \u201cSnapshots\u201d tab in the panel that opens on the right: You can open any of the listed snapshots to see the document as it existed at that time.","title":"Examining Backups"},{"location":"automatic-backups/#restoring-an-older-version","text":"While examining a snapshot, the \u201cShare\u201d menu has additional options; Use the \u201cReplace Current Version\u201d option to revert your document to the version you are looking at. You can also save the snapshot as a new document using the \u201cSave Copy\u201d option.","title":"Restoring an Older Version"},{"location":"automatic-backups/#deleted-documents","text":"When you delete a document, its history of backups is deleted with it. While the document is in Trash, you can still restore it for 30 days. If you do, you will find all the historical snapshots still there. If you choose \u201cDelete Forever\u201d for a document in Trash, or when a document in Trash is automatically purged after 30 days, the backups will be deleted forever with it.","title":"Deleted Documents"},{"location":"browser-support/","text":"Browser Support # Grist is officially supported and regularly tested on modern Firefox and Google Chrome browsers on all desktop platforms. These are available here: Get Firefox Get Chrome Other modern browsers will work to the degree they are standards compliant. In particular, Grist is reported to work on modern Safari and Microsoft Edge. If you encounter errors or unexpected behavior on these browsers, we encourage you to report them to us by emailing us at support@getgrist.com . Mobile Support # You can use Grist quite comfortably in mobile browsers such as Chrome, Safari, and Firefox. On small screens, you can find the list of pages from the button in the bottom bar. For pages with multiple widgets, only the active widget is expanded. Other widgets are collapsed until you touch to expand them. To edit a cell, simply double-tap it. Mobile support is still a work in progress. To get back to the desktop version on a mobile device, open your account menu, and select \u201cToggle Mobile Mode\u201d option:","title":"Browser Support"},{"location":"browser-support/#browser-support","text":"Grist is officially supported and regularly tested on modern Firefox and Google Chrome browsers on all desktop platforms. These are available here: Get Firefox Get Chrome Other modern browsers will work to the degree they are standards compliant. In particular, Grist is reported to work on modern Safari and Microsoft Edge. If you encounter errors or unexpected behavior on these browsers, we encourage you to report them to us by emailing us at support@getgrist.com .","title":"Browser Support"},{"location":"browser-support/#mobile-support","text":"You can use Grist quite comfortably in mobile browsers such as Chrome, Safari, and Firefox. On small screens, you can find the list of pages from the button in the bottom bar. For pages with multiple widgets, only the active widget is expanded. Other widgets are collapsed until you touch to expand them. To edit a cell, simply double-tap it. Mobile support is still a work in progress. To get back to the desktop version on a mobile device, open your account menu, and select \u201cToggle Mobile Mode\u201d option:","title":"Mobile Support"},{"location":"col-refs/","text":"Reference and Reference Lists # Overview # Reference and Reference List columns in Grist allow one table to create an explicit reference to another. In the database world this is similar to a foreign key. In the spreadsheet world this is similar to a VLOOKUP , but much more powerful and easier to use. In this guide we\u2019ll use the term underlying table for the table that lists all available values, and referencing table for the table that uses those values. Creating a new Reference column # Suppose we have a document with two tables, Clients and Projects. The Clients table lists our clients - names, contacts, signing dates - and the Projects table lists projects we do for clients. There are all sorts of things Grist can do for us if we let it know that the Client column in the Projects table is referring to clients listed in the Clients table. We can do this by converting the Client column to a \u201creference column\u201d. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d to \u201cReference\u201d. Adjust the \u201cData from Table\u201d option to be the correct table you want to cross-reference, and the \u201cShow Column\u201d option to match which column of that table you\u2019d like to show. Then hit \u201cApply\u201d when you\u2019re happy with the result. Understanding the reference The column value always references the entire record in the underlying table. The displayed value can be any column from that record, as selected in Show Column . You can also include additional columns to display as explained later. In our example, you can see little link icons appearing in the Client column cells, showing that they have been successfully cross-referenced with the Clients table. Once the column type is set, you can start typing into it or double-click it to see a dropdown list of all available values. Note that the table Clients and the column Client are related by the column type rather than by name. They can be named anything. Spotting reference columns You can tell that the values in a column represent a reference by the link icon that appears next to the values. If you accidentally type in a value that is not present in the Clients table, its value will be highlighted as invalid: Adding values to a Reference column # Sometimes it\u2019s useful to add a new value to the dropdown list without having to switch to the underlying table. Reference columns make it easy! Just type in the value you want add and select the + value in the dropdown list. Grist will automatically add a new record containing this value to the underlying table and insert the proper reference: Converting Text column to Reference # When working with existing data, it\u2019s common to have existing text values that should really be reference values. Don\u2019t worry, conversion is simple! Just change the column type to Reference and Grist will automatically find and substitute matching values for references. If some values are not found, they\u2019ll be shown as invalid. You can then either add them to the underlying table or select the proper values for them. In this example, the first three values match perfectly, but Forest Labs is invalid because it doesn\u2019t exist in the Clients table: Including multiple fields from a reference # A big benefit of reference columns is that they allow you to easily bring in multiple columns from the underlying table. In our example, if you wanted to bring in $Client.Contact to the Projects table, you can just select the Contact column from the Add Referenced Columns section and it will be automatically added to the Projects table: If you\u2019re comfortable using formulas, you can see that the added column is just the formula =$Client.Contact . If you were so inclined, you could achieve the same result by manually adding the formula column. You can also use any other fields from Clients table by referencing $Client in formulas in the Projects table. Note that in formulas, we use the name of the reference column ( $Client ) to refer to a linked record, not the name of the table (which is Clients here). Don\u2019t let the similarity of the names in this example confuse you. Using references in formulas You may have noticed that the underlying table is Clients (plural) but the formula is $Client.Contact (singular). That\u2019s because the formula refers to the referencing column, not the underlying table. In our example, the referencing column is Client . Creating a new Reference List column # So far our example has only dealt with projects that have a single client. Suppose that we also have projects with multiple clients, and we\u2019d like to maintain references to them all from the Client column of the Projects table. We can let Grist know that the Client column contains multiple references by changing its type to \u201cReference List\u201d. This column type can reference multiple records, and can also be thought of as a multi-select. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d of Client to \u201cReference List\u201d. Grist will automatically convert any of your existing references to reference lists. Once you\u2019re happy with the result, just hit \u201cApply\u201d and the Client column will be ready to accept as many clients as your projects need. Editing values in a Reference List column # To make changes to a Reference List cell, simply double-click the cell or press the Enter key after you have selected the cell you want to edit. You can also start typing after selecting a cell if you\u2019d like to write over any existing contents. Doing so will open an editor like the one in the example below. Like with Reference columns, the autocomplete menu will populate with suggestions as you type. If you type in a value that\u2019s not present in the referenced table, you can select the + value to add a new row to the referenced table with your value. To delete existing references, simply press the Backspace key, or move your cursor over a reference and click the X icon. You can also rearrange references in the editor by dragging them with your mouse. To save your changes and close the editor, either press Enter or Tab , or click anywhere outside the editor. To close the editor and discard any changes you\u2019ve made, press Escape . Understanding reference columns # Cells in a reference column always identify an entire record in the referenced table. For convenience, you may select which column from that record to show by setting the \u201cSHOW COLUMN\u201d. However, the cell\u2019s value is always a record\u2019s unique ID. Similarly, Reference Lists store a list of record ids. What does that really mean? Let\u2019s take a look at the Class Enrollment template. In the Classes table, the Instructor column is a reference column that references data from the Staff table. Full Name is selected under \u2018Show Column\u2019 and is used as a label to represent the record from the Staff table that is being referenced here. We can change that label to any other value contained within the record. Let\u2019s change it to \u2018Row ID\u2019. The row ID is what is actually being stored within the Reference or Reference List column. With this ID, we can fetch any data associated with this record. In the first row of the Classes table, we see Staff[2] as the value in the Instructor column. This represents the record in the Staff table with Row ID = 2 . We can navigate to the Staff table and see which record is assigned Row ID = 2 . To view a record\u2019s unique ID, add a new column with the formula = $id . We can see that the value in the Full Name column for the record with Row ID = 2 is Dowbakin, Daniella . If we revert back to our original settings for the Instructor column of the Classes table, where Full Name was selected under Show Column, we see that the Full Name value associated with Staff[2] is Dowbakin, Daniella . Filtering Reference choices in dropdown # When entering data into a reference column you will see a dropdown list of all available values to choose from. Sometimes the list can get long, and in some cases confusing. For example, say you\u2019re tracking population changes in the 1,000 most populous world cities. When entering a city into the reference column for city selection, the dropdown lists all 1,000 cities. It would be useful if the dropdown list of city choices were filtered based on the country selected in the Country column. To filter a reference column\u2019s dropdown list, select the reference column then set a \u201cDropdown Condition\u201c in the Creator Panel under the \u201cColumn\u201d tab. You can filter a dropdown\u2019s choice by writing a condition as a formula. The attribute choice refers to choices in the dropdown. In this case the formula is choice.Country == $Country . Why did that work? The city column is a reference column pointing to a Cities table that matches countries and cities. That table looks like this. The formula condition choice.Country == $Country is looking up each choice\u2019s country in the Cities table using a reference lookup , then it compares those countries to the value entered in the Country column of the Population Rankings table. The dropdown now lists only choices (aka cities) whose country equals the country entered in Country column. The choice attribute can also be used when setting dropdown filter conditions for Choice and Choice List columns. Note that because reference dropdown filtering is written as formulas, filtering can be very flexible and granular. Users experienced with access rules may notice similarities in how to think about writing these formulas.","title":"Reference columns"},{"location":"col-refs/#reference-and-reference-lists","text":"","title":"Reference and Reference Lists"},{"location":"col-refs/#overview","text":"Reference and Reference List columns in Grist allow one table to create an explicit reference to another. In the database world this is similar to a foreign key. In the spreadsheet world this is similar to a VLOOKUP , but much more powerful and easier to use. In this guide we\u2019ll use the term underlying table for the table that lists all available values, and referencing table for the table that uses those values.","title":"Overview"},{"location":"col-refs/#creating-a-new-reference-column","text":"Suppose we have a document with two tables, Clients and Projects. The Clients table lists our clients - names, contacts, signing dates - and the Projects table lists projects we do for clients. There are all sorts of things Grist can do for us if we let it know that the Client column in the Projects table is referring to clients listed in the Clients table. We can do this by converting the Client column to a \u201creference column\u201d. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d to \u201cReference\u201d. Adjust the \u201cData from Table\u201d option to be the correct table you want to cross-reference, and the \u201cShow Column\u201d option to match which column of that table you\u2019d like to show. Then hit \u201cApply\u201d when you\u2019re happy with the result. Understanding the reference The column value always references the entire record in the underlying table. The displayed value can be any column from that record, as selected in Show Column . You can also include additional columns to display as explained later. In our example, you can see little link icons appearing in the Client column cells, showing that they have been successfully cross-referenced with the Clients table. Once the column type is set, you can start typing into it or double-click it to see a dropdown list of all available values. Note that the table Clients and the column Client are related by the column type rather than by name. They can be named anything. Spotting reference columns You can tell that the values in a column represent a reference by the link icon that appears next to the values. If you accidentally type in a value that is not present in the Clients table, its value will be highlighted as invalid:","title":"Creating a new Reference column"},{"location":"col-refs/#adding-values-to-a-reference-column","text":"Sometimes it\u2019s useful to add a new value to the dropdown list without having to switch to the underlying table. Reference columns make it easy! Just type in the value you want add and select the + value in the dropdown list. Grist will automatically add a new record containing this value to the underlying table and insert the proper reference:","title":"Adding values to a Reference column"},{"location":"col-refs/#converting-text-column-to-reference","text":"When working with existing data, it\u2019s common to have existing text values that should really be reference values. Don\u2019t worry, conversion is simple! Just change the column type to Reference and Grist will automatically find and substitute matching values for references. If some values are not found, they\u2019ll be shown as invalid. You can then either add them to the underlying table or select the proper values for them. In this example, the first three values match perfectly, but Forest Labs is invalid because it doesn\u2019t exist in the Clients table:","title":"Converting Text column to Reference"},{"location":"col-refs/#including-multiple-fields-from-a-reference","text":"A big benefit of reference columns is that they allow you to easily bring in multiple columns from the underlying table. In our example, if you wanted to bring in $Client.Contact to the Projects table, you can just select the Contact column from the Add Referenced Columns section and it will be automatically added to the Projects table: If you\u2019re comfortable using formulas, you can see that the added column is just the formula =$Client.Contact . If you were so inclined, you could achieve the same result by manually adding the formula column. You can also use any other fields from Clients table by referencing $Client in formulas in the Projects table. Note that in formulas, we use the name of the reference column ( $Client ) to refer to a linked record, not the name of the table (which is Clients here). Don\u2019t let the similarity of the names in this example confuse you. Using references in formulas You may have noticed that the underlying table is Clients (plural) but the formula is $Client.Contact (singular). That\u2019s because the formula refers to the referencing column, not the underlying table. In our example, the referencing column is Client .","title":"Including multiple fields from a reference"},{"location":"col-refs/#creating-a-new-reference-list-column","text":"So far our example has only dealt with projects that have a single client. Suppose that we also have projects with multiple clients, and we\u2019d like to maintain references to them all from the Client column of the Projects table. We can let Grist know that the Client column contains multiple references by changing its type to \u201cReference List\u201d. This column type can reference multiple records, and can also be thought of as a multi-select. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d of Client to \u201cReference List\u201d. Grist will automatically convert any of your existing references to reference lists. Once you\u2019re happy with the result, just hit \u201cApply\u201d and the Client column will be ready to accept as many clients as your projects need.","title":"Creating a new Reference List column"},{"location":"col-refs/#editing-values-in-a-reference-list-column","text":"To make changes to a Reference List cell, simply double-click the cell or press the Enter key after you have selected the cell you want to edit. You can also start typing after selecting a cell if you\u2019d like to write over any existing contents. Doing so will open an editor like the one in the example below. Like with Reference columns, the autocomplete menu will populate with suggestions as you type. If you type in a value that\u2019s not present in the referenced table, you can select the + value to add a new row to the referenced table with your value. To delete existing references, simply press the Backspace key, or move your cursor over a reference and click the X icon. You can also rearrange references in the editor by dragging them with your mouse. To save your changes and close the editor, either press Enter or Tab , or click anywhere outside the editor. To close the editor and discard any changes you\u2019ve made, press Escape .","title":"Editing values in a Reference List column"},{"location":"col-refs/#understanding-reference-columns","text":"Cells in a reference column always identify an entire record in the referenced table. For convenience, you may select which column from that record to show by setting the \u201cSHOW COLUMN\u201d. However, the cell\u2019s value is always a record\u2019s unique ID. Similarly, Reference Lists store a list of record ids. What does that really mean? Let\u2019s take a look at the Class Enrollment template. In the Classes table, the Instructor column is a reference column that references data from the Staff table. Full Name is selected under \u2018Show Column\u2019 and is used as a label to represent the record from the Staff table that is being referenced here. We can change that label to any other value contained within the record. Let\u2019s change it to \u2018Row ID\u2019. The row ID is what is actually being stored within the Reference or Reference List column. With this ID, we can fetch any data associated with this record. In the first row of the Classes table, we see Staff[2] as the value in the Instructor column. This represents the record in the Staff table with Row ID = 2 . We can navigate to the Staff table and see which record is assigned Row ID = 2 . To view a record\u2019s unique ID, add a new column with the formula = $id . We can see that the value in the Full Name column for the record with Row ID = 2 is Dowbakin, Daniella . If we revert back to our original settings for the Instructor column of the Classes table, where Full Name was selected under Show Column, we see that the Full Name value associated with Staff[2] is Dowbakin, Daniella .","title":"Understanding reference columns"},{"location":"col-refs/#filtering-reference-choices-in-dropdown","text":"When entering data into a reference column you will see a dropdown list of all available values to choose from. Sometimes the list can get long, and in some cases confusing. For example, say you\u2019re tracking population changes in the 1,000 most populous world cities. When entering a city into the reference column for city selection, the dropdown lists all 1,000 cities. It would be useful if the dropdown list of city choices were filtered based on the country selected in the Country column. To filter a reference column\u2019s dropdown list, select the reference column then set a \u201cDropdown Condition\u201c in the Creator Panel under the \u201cColumn\u201d tab. You can filter a dropdown\u2019s choice by writing a condition as a formula. The attribute choice refers to choices in the dropdown. In this case the formula is choice.Country == $Country . Why did that work? The city column is a reference column pointing to a Cities table that matches countries and cities. That table looks like this. The formula condition choice.Country == $Country is looking up each choice\u2019s country in the Cities table using a reference lookup , then it compares those countries to the value entered in the Country column of the Population Rankings table. The dropdown now lists only choices (aka cities) whose country equals the country entered in Country column. The choice attribute can also be used when setting dropdown filter conditions for Choice and Choice List columns. Note that because reference dropdown filtering is written as formulas, filtering can be very flexible and granular. Users experienced with access rules may notice similarities in how to think about writing these formulas.","title":"Filtering Reference choices in dropdown"},{"location":"col-transform/","text":"Column Transformations # Grist offers two ways to transform all values in a column. One is to change the type of the column, and the other is to apply a formula-based transformation. Type conversions # When converting between different column types, Grist has sensible default behavior, but makes that behavior easy to revise. For example, suppose you have a column of integers. To convert that column to text, open the column options as described in Specifying a type , and find the column type section. Change the column type to text in the dropdown. You\u2019ll notice that a \u201ccancel/revise/apply\u201d dialog opens beside the dropdown. To change how the conversion is done, click Revise . You\u2019ll see a formula box with the default conversion method, grist.Text.typeConvert($tally) . This means \u201cdo default conversion to text for the tally column\u201d. you can replace this with any formula you like. For example: Code for converting to unicode tally lines is left as an exercise to the reader. To preview the results of the conversion, click \u201cpreview\u201d. When you are satisfied with the conversion, click \u201capply\u201d. To abandon the conversion, click \u201ccancel\u201d. Formula-based transforms # Spreadsheets are convenient tools for cleaning up data using formulas . For example, imagine you had zip codes that have lost leading zeros - you can easily reformat them with a quick formula: We could now freeze the results and delete the original data if we don\u2019t need it anymore. If you know you\u2019re going to throw away the original data like this, Grist offers column transformations as a faster way to systematically modify all cells of a column. Find the \u201cTransform\u201d section at the bottom of the column options side panel (see Columns for how to open this panel). When you click the orange \u201clightning\u201d button, Grist prompts you with a formula, return $zip in this case. You can edit this formula to make some change to the selected column. For example return $zip + 1 would add one to the zip code. You can preview the effect your formula would have, and when you are happy, hit \u201cApply\u201d. In our case, where we want to add leading zeros, we\u2019ll need to first change our column type to be Text (assuming it is currently Integer - if it Numeric convert to Integer first and then to Text to avoid decimal points). Once done, we can use our formula for adding leading zeros: When happy, press \u201cApply\u201d to replace the cell values with their new versions. Likewise, the response column could be transformed with the formula into true/false values with $response[0] == 'y' , and then set as a toggle column .","title":"Transformations"},{"location":"col-transform/#column-transformations","text":"Grist offers two ways to transform all values in a column. One is to change the type of the column, and the other is to apply a formula-based transformation.","title":""},{"location":"col-transform/#type-conversions","text":"When converting between different column types, Grist has sensible default behavior, but makes that behavior easy to revise. For example, suppose you have a column of integers. To convert that column to text, open the column options as described in Specifying a type , and find the column type section. Change the column type to text in the dropdown. You\u2019ll notice that a \u201ccancel/revise/apply\u201d dialog opens beside the dropdown. To change how the conversion is done, click Revise . You\u2019ll see a formula box with the default conversion method, grist.Text.typeConvert($tally) . This means \u201cdo default conversion to text for the tally column\u201d. you can replace this with any formula you like. For example: Code for converting to unicode tally lines is left as an exercise to the reader. To preview the results of the conversion, click \u201cpreview\u201d. When you are satisfied with the conversion, click \u201capply\u201d. To abandon the conversion, click \u201ccancel\u201d.","title":"Type conversions"},{"location":"col-transform/#formula-based-transforms","text":"Spreadsheets are convenient tools for cleaning up data using formulas . For example, imagine you had zip codes that have lost leading zeros - you can easily reformat them with a quick formula: We could now freeze the results and delete the original data if we don\u2019t need it anymore. If you know you\u2019re going to throw away the original data like this, Grist offers column transformations as a faster way to systematically modify all cells of a column. Find the \u201cTransform\u201d section at the bottom of the column options side panel (see Columns for how to open this panel). When you click the orange \u201clightning\u201d button, Grist prompts you with a formula, return $zip in this case. You can edit this formula to make some change to the selected column. For example return $zip + 1 would add one to the zip code. You can preview the effect your formula would have, and when you are happy, hit \u201cApply\u201d. In our case, where we want to add leading zeros, we\u2019ll need to first change our column type to be Text (assuming it is currently Integer - if it Numeric convert to Integer first and then to Text to avoid decimal points). Once done, we can use our formula for adding leading zeros: When happy, press \u201cApply\u201d to replace the cell values with their new versions. Likewise, the response column could be transformed with the formula into true/false values with $response[0] == 'y' , and then set as a toggle column .","title":"Formula-based transforms"},{"location":"col-types/","text":"Columns and data types # Adding and removing columns # Every Grist table, when first created, has three columns called A, B, and C. To rename a column, hover on the column header, click on the drop-down, then select \u201cRename column\u201d (you can also just click on the column header twice). To delete a column, hover on the column header, click on the drop-down, then select \u201cDelete column\u201d. To add a column, click on the \u201c+\u201d symbol in the header row to open the \u201cAdd Column\u201d menu. The first option in the menu, \u201cAdd Column\u201d, will add a new, empty data column to your table. If you know what column type you need, the next option allows you to assign a type for your new column. Select a column type from the expanded menu. The third option, \u201cAdd formula column\u201d, will add a new formula column with the formula entry box immediately opened so you can begin entering your formula without additional clicks. \u201cHidden Columns\u201d expands to show a list of columns hidden from this view that can be quickly added back. \u201cLookups\u201d allows you to add data columns from related tables. You can use reference columns to relate data in different tables. Learn more about References and Lookups . \u201cShortcuts\u201d lists the most frequently used trigger formula functions. Learn more about each shortcut option at the links below: Timestamp Authorship Detect Duplicates in\u2026 UUID Reordering columns # To reorder a column, first select the column if it isn\u2019t already selected, by clicking on the column header. Next, click and hold on the column header. After a second or two, you\u2019ll be able to drag the entire column to its new location. Another way to reorder columns is via the widget options: In the visible columns section, the columns can be dragged around freely to reorder them. You can also hide columns here. Renaming columns # You can rename columns in several ways. One way is to double click a column header. Then, you can rename the column or add a column description. You can also hover on the column header, click on the drop-down, then select \u201cRename Column\u201d. This opens the same pop up seen above. Selecting \u201cColumn Options\u201d in the same drop-down opens the creator panel. From here, you can edit the Column Label, shown at the top or add a description. A bonus with this method is that you can also control the identifier given to the column in formulas. By default this is based on the field name, with any characters Python doesn\u2019t like replaced with \u201c_\u201c, and a number added if needed to keep the name unique within your table. If you don\u2019t like this identifier, you can change it, though it will still need to be Python-friendly. Click the link icon to make the ID field editable, then enter the new Column ID. Formatting columns # Header and cell styles can be modified under the Column tab of the creator panel. When you open the styling menu, you have the option to apply text formatting as well as text and fill colors. If you want more color options, click the fill color box then find the shade you want. If you have a specific color you wish to use, you can enter the hex code, RGB or HSL values. Cell style can also be changed based on conditional rules. Learn more about Conditional Formatting . Specifying a type # Grist columns have types, similar to other spreadsheets or databases. The type of a column controls its appearance and the help Grist will offer you when editing cells. When you create a new column, it initially has the Any type. When you enter the column\u2019s first cell, Grist tries to narrow this type. If you enter a number, the column will be changed to Numeric type, which is right-aligned by default. If you enter something that doesn\u2019t look like a number, the column will be changed to Text type, which is left-aligned by default. To inspect the type of a column, hover over the column header, then click on the drop-down, then select \u201cColumn Options\u201d. The \u201cColumn Type\u201d section is what you are looking for. You will often want to control the column type manually. You can change it in the \u201cColumn Type\u201d section. For example, here we set a column full of \u201cyes\u201d and \u201cno\u201d responses to be of type Toggle : One advantage of doing so is that Grist can now offer you ways to visualize the column that are specialized to on/off style values. Each column type has different options in the \u201cCell Format\u201d section of the side panel: Regardless of the column type, you can enter any value in cells. If a value entered is incompatible with the defined type, the cell will be highlighted with an error (and columns referencing the invalid value will also display an error): Supported types # Grist supports the following types: Type Description Text ( Default ) Any string of text. Numeric Floating point numbers. Integer Integers (whole numbers). Toggle Boolean (True / False) Date Valid date (without a time component). DateTime Valid date + time. Choice Single value from a list of pre-defined valid values. Choice List Multiple values from a list of pre-defined valid values. Reference A reference column to another table. Reference List A list of references to another table. Attachment Cells where you can place files or images. Text columns # You can put any text you like in this type of column. For formatting, you can control alignment and word-wrap, text color and background color. If the column is used for storing web links, you can turn on \u201cHyperLink\u201d formatting to make links prettier and to include a clickable link icon. Hyperlinks # When a Text column uses \u201cHyperLink\u201d formatting, values get formatted like so: https://getgrist.com will show https://getgrist.com . Grist Labs https://getgrist.com will show Grist Labs (linking to \u201chttps://getgrist.com\u201d with \u201cGrist Labs\u201d as the text). Email Help mailto:support@getgrist.com will show Email Help , a link which would open an email program to compose an email to support@getgrist.com. In general, the value until the last space is used as the link text, while the last word is used as the link destination. Link formatting is particularly useful when links are generated using a formula such as: $Company + \" \" + $Website Numeric columns # This type is for numbers, including floating-point numbers. In addition to controlling alignment and color, you can choose the number format, and the minimum and maximum number of digits to show after the decimal point. Choosing the \u201cSpinner\u201d option for CELL FORMAT will show arrows in each cell for increasing/decreasing the number. The options under NUMBER FORMAT include: $ : Format for currency amounts, such as dollars or euro. Selecting the $ will add a currency prefix, thousands separators, and default to 2 digits after the decimal point. It will also open a currency selector for international currencies. Setting Default Currency You can set a document\u2019s default timezone, locale, and currency in Document Settings . , : Turn on the display of thousands separators. % : Show numbers as percentages. E.g. \u201c0.5\u201d would show as \u201c50%\u201d. Exp : Show numbers in exponential (or scientific) notation. E.g. \u201c1234\u201d would be shown as \u201c1.234E3\u201d. (-) : Show negative numbers in parentheses, without a leading minus sign. This is commonly used in accounting, and usually combined with $ or , formats. Integer columns # This is strictly for whole numbers. It has the same options as the numeric type. Toggle columns # This type is for storing true/false values. The values can be shown as text, checkboxes, or switches. See also example in Specifying a type . Date columns # This type is for storing calendar dates (without a time of day component). More details in Working with dates . You can choose the format for dates, see the date formatting reference . DateTime columns # This type is for storing calendar dates plus time of day. More details in Working with dates . You can choose the format for dates, see the date and time formatting reference . You can also specify the timezone to display for. If you\u2019d like to set a default timezone for your document, you can do so in Document Settings . Choice columns # This type is for storing one of a set of valid values, where you get to specify the available values. There\u2019s an example of using this type of column in the Lightweight CRM example . If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. You can add or remove choices by either clicking Edit or on the Choices box. To add a choice, type its value in the text field below the other choices and press Enter . To remove a choice, click the delete icon to the right of the choice or select the choice by clicking on it, and then press Backspace / Delete . To apply/save your changes, you can either click the Save button or press Enter . To discard your changes, you can either click the Cancel button or press Escape . Clicking the color dropdown to the left of a choice will open a color picker for customizing the fill and text color of a choice. Changes to colors are reflected inside cells and throughout the rest of your document once you save your changes. Choices can be re-arranged by clicking and dragging them, which determines the order in which they appear when typing into a cell. You can also rename a choice by clicking on it and typing a new name. Renaming a choice will also rename all the values that are used in your document. The configuration editor supports many convenient keyboard shortcuts. You can press the Up Arrow and Down Arrow keys to navigate between selected choices; hold the Shift key to bulk select adjacent choices while clicking or using the arrow keys; and hold the Command / Control key to multi-select choices while clicking. To select all choices, you can press Command / Control + A . Undo and redo are also supported in the configuration editor. While your cursor has focus on the editor text field, you can press Command / Control + Z to undo your last change, and Command / Control + Shift + Z to redo it. The configuration editor also supports copy and paste. To copy, select the choices you want copied and press Command / Control + C . To paste, focus on the text field and press Command / Control + V . Choices are pasted in bulk if the clipboard contains multiple lines of text. Choices can also be copied from one column\u2019s configuration editor and pasted into another, which will copy over both the values and their configured colors. When typing into a Choice column cell, your configured choices will be shown in an autocomplete menu. You can either click on a choice, or use the arrow keys and Enter to add a choice to a cell. If your input is not one of the valid choices, Grist will display a menu option for conveniently adding it as a valid choice and into the cell in one step. Choice List columns # This type is for storing multiple values from a set of valid values, where you get to specify the available values. If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. Choice List columns are configured the same way Choice columns are, and support the same level of customization and keyboard shortcuts. They differ in the number of choices they allow to be entered in each cell. While Choice columns only allow at most one value in a cell, Choice List columns allow many. Like with Choice columns, when typing into a Choice List column cell, the valid choices will be shown in an autocomplete menu. Once you\u2019ve selected a value, you can continue adding choices to the same cell. Choices can be re-arranged inside their cells by clicking and dragging them while the cell is being edited. You can also use the arrow keys and the Delete key to navigate and delete choices, or simply click the delete icon when hovering your cursor over a choice. Filtering Choice and Choice List columns\u2019 dropdown lists When entering data into a Choice or Choice List column you will see a dropdown list of all available choices. Sometimes it would be useful to filter the dropdown list based on a condition, such as the value in another cell. Writing conditions to filter choice dropdown lists is similar to filtering reference column\u2019s dropdown lists . Reference columns # This sets up a cross-reference to another table. You can specify the table to reference, and a column within that table to show. There\u2019s a lot you can do with this kind of column, see Reference columns for details. Reference List columns # Like Reference columns , but can store multiple references in a single cell. There\u2019s a lot you can do with this kind of column, see Reference columns for details. Attachment columns # This column type lets you insert entire files and images in cells. When images are added in cells, a preview thumbnail is shown in the cell. The \u201cSize\u201d bar gives control of the scale of this thumbnail. When you create an attachment column, cells of that column will have a paperclip icon: When you click on a paperclip icon, you can select a file to attach. If it is an image, you\u2019ll see a thumbnail of it in the cell. If you hover over the image, you\u2019ll see a paperclip icon again, which you can use to add more files to the same cell. You\u2019ll also see an \u201copen-eye\u201d icon, which when clicked brings up a larger view of all of the cell\u2019s attachments, and gives you a way to rename them, download them, or remove them.","title":"Columns & types"},{"location":"col-types/#columns-and-data-types","text":"","title":"Columns and data types"},{"location":"col-types/#adding-and-removing-columns","text":"Every Grist table, when first created, has three columns called A, B, and C. To rename a column, hover on the column header, click on the drop-down, then select \u201cRename column\u201d (you can also just click on the column header twice). To delete a column, hover on the column header, click on the drop-down, then select \u201cDelete column\u201d. To add a column, click on the \u201c+\u201d symbol in the header row to open the \u201cAdd Column\u201d menu. The first option in the menu, \u201cAdd Column\u201d, will add a new, empty data column to your table. If you know what column type you need, the next option allows you to assign a type for your new column. Select a column type from the expanded menu. The third option, \u201cAdd formula column\u201d, will add a new formula column with the formula entry box immediately opened so you can begin entering your formula without additional clicks. \u201cHidden Columns\u201d expands to show a list of columns hidden from this view that can be quickly added back. \u201cLookups\u201d allows you to add data columns from related tables. You can use reference columns to relate data in different tables. Learn more about References and Lookups . \u201cShortcuts\u201d lists the most frequently used trigger formula functions. Learn more about each shortcut option at the links below: Timestamp Authorship Detect Duplicates in\u2026 UUID","title":"Adding and removing columns"},{"location":"col-types/#reordering-columns","text":"To reorder a column, first select the column if it isn\u2019t already selected, by clicking on the column header. Next, click and hold on the column header. After a second or two, you\u2019ll be able to drag the entire column to its new location. Another way to reorder columns is via the widget options: In the visible columns section, the columns can be dragged around freely to reorder them. You can also hide columns here.","title":"Reordering columns"},{"location":"col-types/#renaming-columns","text":"You can rename columns in several ways. One way is to double click a column header. Then, you can rename the column or add a column description. You can also hover on the column header, click on the drop-down, then select \u201cRename Column\u201d. This opens the same pop up seen above. Selecting \u201cColumn Options\u201d in the same drop-down opens the creator panel. From here, you can edit the Column Label, shown at the top or add a description. A bonus with this method is that you can also control the identifier given to the column in formulas. By default this is based on the field name, with any characters Python doesn\u2019t like replaced with \u201c_\u201c, and a number added if needed to keep the name unique within your table. If you don\u2019t like this identifier, you can change it, though it will still need to be Python-friendly. Click the link icon to make the ID field editable, then enter the new Column ID.","title":"Renaming columns"},{"location":"col-types/#formatting-columns","text":"Header and cell styles can be modified under the Column tab of the creator panel. When you open the styling menu, you have the option to apply text formatting as well as text and fill colors. If you want more color options, click the fill color box then find the shade you want. If you have a specific color you wish to use, you can enter the hex code, RGB or HSL values. Cell style can also be changed based on conditional rules. Learn more about Conditional Formatting .","title":"Formatting columns"},{"location":"col-types/#specifying-a-type","text":"Grist columns have types, similar to other spreadsheets or databases. The type of a column controls its appearance and the help Grist will offer you when editing cells. When you create a new column, it initially has the Any type. When you enter the column\u2019s first cell, Grist tries to narrow this type. If you enter a number, the column will be changed to Numeric type, which is right-aligned by default. If you enter something that doesn\u2019t look like a number, the column will be changed to Text type, which is left-aligned by default. To inspect the type of a column, hover over the column header, then click on the drop-down, then select \u201cColumn Options\u201d. The \u201cColumn Type\u201d section is what you are looking for. You will often want to control the column type manually. You can change it in the \u201cColumn Type\u201d section. For example, here we set a column full of \u201cyes\u201d and \u201cno\u201d responses to be of type Toggle : One advantage of doing so is that Grist can now offer you ways to visualize the column that are specialized to on/off style values. Each column type has different options in the \u201cCell Format\u201d section of the side panel: Regardless of the column type, you can enter any value in cells. If a value entered is incompatible with the defined type, the cell will be highlighted with an error (and columns referencing the invalid value will also display an error):","title":"Specifying a type"},{"location":"col-types/#supported-types","text":"Grist supports the following types: Type Description Text ( Default ) Any string of text. Numeric Floating point numbers. Integer Integers (whole numbers). Toggle Boolean (True / False) Date Valid date (without a time component). DateTime Valid date + time. Choice Single value from a list of pre-defined valid values. Choice List Multiple values from a list of pre-defined valid values. Reference A reference column to another table. Reference List A list of references to another table. Attachment Cells where you can place files or images.","title":"Supported types"},{"location":"col-types/#text-columns","text":"You can put any text you like in this type of column. For formatting, you can control alignment and word-wrap, text color and background color. If the column is used for storing web links, you can turn on \u201cHyperLink\u201d formatting to make links prettier and to include a clickable link icon.","title":"Text columns"},{"location":"col-types/#hyperlinks","text":"When a Text column uses \u201cHyperLink\u201d formatting, values get formatted like so: https://getgrist.com will show https://getgrist.com . Grist Labs https://getgrist.com will show Grist Labs (linking to \u201chttps://getgrist.com\u201d with \u201cGrist Labs\u201d as the text). Email Help mailto:support@getgrist.com will show Email Help , a link which would open an email program to compose an email to support@getgrist.com. In general, the value until the last space is used as the link text, while the last word is used as the link destination. Link formatting is particularly useful when links are generated using a formula such as: $Company + \" \" + $Website","title":"Hyperlinks"},{"location":"col-types/#numeric-columns","text":"This type is for numbers, including floating-point numbers. In addition to controlling alignment and color, you can choose the number format, and the minimum and maximum number of digits to show after the decimal point. Choosing the \u201cSpinner\u201d option for CELL FORMAT will show arrows in each cell for increasing/decreasing the number. The options under NUMBER FORMAT include: $ : Format for currency amounts, such as dollars or euro. Selecting the $ will add a currency prefix, thousands separators, and default to 2 digits after the decimal point. It will also open a currency selector for international currencies. Setting Default Currency You can set a document\u2019s default timezone, locale, and currency in Document Settings . , : Turn on the display of thousands separators. % : Show numbers as percentages. E.g. \u201c0.5\u201d would show as \u201c50%\u201d. Exp : Show numbers in exponential (or scientific) notation. E.g. \u201c1234\u201d would be shown as \u201c1.234E3\u201d. (-) : Show negative numbers in parentheses, without a leading minus sign. This is commonly used in accounting, and usually combined with $ or , formats.","title":"Numeric columns"},{"location":"col-types/#integer-columns","text":"This is strictly for whole numbers. It has the same options as the numeric type.","title":"Integer columns"},{"location":"col-types/#toggle-columns","text":"This type is for storing true/false values. The values can be shown as text, checkboxes, or switches. See also example in Specifying a type .","title":"Toggle columns"},{"location":"col-types/#date-columns","text":"This type is for storing calendar dates (without a time of day component). More details in Working with dates . You can choose the format for dates, see the date formatting reference .","title":"Date columns"},{"location":"col-types/#datetime-columns","text":"This type is for storing calendar dates plus time of day. More details in Working with dates . You can choose the format for dates, see the date and time formatting reference . You can also specify the timezone to display for. If you\u2019d like to set a default timezone for your document, you can do so in Document Settings .","title":"DateTime columns"},{"location":"col-types/#choice-columns","text":"This type is for storing one of a set of valid values, where you get to specify the available values. There\u2019s an example of using this type of column in the Lightweight CRM example . If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. You can add or remove choices by either clicking Edit or on the Choices box. To add a choice, type its value in the text field below the other choices and press Enter . To remove a choice, click the delete icon to the right of the choice or select the choice by clicking on it, and then press Backspace / Delete . To apply/save your changes, you can either click the Save button or press Enter . To discard your changes, you can either click the Cancel button or press Escape . Clicking the color dropdown to the left of a choice will open a color picker for customizing the fill and text color of a choice. Changes to colors are reflected inside cells and throughout the rest of your document once you save your changes. Choices can be re-arranged by clicking and dragging them, which determines the order in which they appear when typing into a cell. You can also rename a choice by clicking on it and typing a new name. Renaming a choice will also rename all the values that are used in your document. The configuration editor supports many convenient keyboard shortcuts. You can press the Up Arrow and Down Arrow keys to navigate between selected choices; hold the Shift key to bulk select adjacent choices while clicking or using the arrow keys; and hold the Command / Control key to multi-select choices while clicking. To select all choices, you can press Command / Control + A . Undo and redo are also supported in the configuration editor. While your cursor has focus on the editor text field, you can press Command / Control + Z to undo your last change, and Command / Control + Shift + Z to redo it. The configuration editor also supports copy and paste. To copy, select the choices you want copied and press Command / Control + C . To paste, focus on the text field and press Command / Control + V . Choices are pasted in bulk if the clipboard contains multiple lines of text. Choices can also be copied from one column\u2019s configuration editor and pasted into another, which will copy over both the values and their configured colors. When typing into a Choice column cell, your configured choices will be shown in an autocomplete menu. You can either click on a choice, or use the arrow keys and Enter to add a choice to a cell. If your input is not one of the valid choices, Grist will display a menu option for conveniently adding it as a valid choice and into the cell in one step.","title":"Choice columns"},{"location":"col-types/#choice-list-columns","text":"This type is for storing multiple values from a set of valid values, where you get to specify the available values. If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. Choice List columns are configured the same way Choice columns are, and support the same level of customization and keyboard shortcuts. They differ in the number of choices they allow to be entered in each cell. While Choice columns only allow at most one value in a cell, Choice List columns allow many. Like with Choice columns, when typing into a Choice List column cell, the valid choices will be shown in an autocomplete menu. Once you\u2019ve selected a value, you can continue adding choices to the same cell. Choices can be re-arranged inside their cells by clicking and dragging them while the cell is being edited. You can also use the arrow keys and the Delete key to navigate and delete choices, or simply click the delete icon when hovering your cursor over a choice. Filtering Choice and Choice List columns\u2019 dropdown lists When entering data into a Choice or Choice List column you will see a dropdown list of all available choices. Sometimes it would be useful to filter the dropdown list based on a condition, such as the value in another cell. Writing conditions to filter choice dropdown lists is similar to filtering reference column\u2019s dropdown lists .","title":"Choice List columns"},{"location":"col-types/#reference-columns","text":"This sets up a cross-reference to another table. You can specify the table to reference, and a column within that table to show. There\u2019s a lot you can do with this kind of column, see Reference columns for details.","title":"Reference columns"},{"location":"col-types/#reference-list-columns","text":"Like Reference columns , but can store multiple references in a single cell. There\u2019s a lot you can do with this kind of column, see Reference columns for details.","title":"Reference List columns"},{"location":"col-types/#attachment-columns","text":"This column type lets you insert entire files and images in cells. When images are added in cells, a preview thumbnail is shown in the cell. The \u201cSize\u201d bar gives control of the scale of this thumbnail. When you create an attachment column, cells of that column will have a paperclip icon: When you click on a paperclip icon, you can select a file to attach. If it is an image, you\u2019ll see a thumbnail of it in the cell. If you hover over the image, you\u2019ll see a paperclip icon again, which you can use to add more files to the same cell. You\u2019ll also see an \u201copen-eye\u201d icon, which when clicked brings up a larger view of all of the cell\u2019s attachments, and gives you a way to rename them, download them, or remove them.","title":"Attachment columns"},{"location":"conditional-formatting/","text":"Conditional Formatting # Cell style can change based on conditional rules. Conditional rules are written as formulas. Conditional formatting can apply to an entire row or cells in a column. To add conditional formatting to a particular column, select the column, go to the CELL STYLE section of the creator panel under the Column tab, and click on Add conditional style . In this example, we have a list of dog breeders who have raised champion thoroughbreds. Let\u2019s apply conditional formatting to the Breeder column based on the number of champion dogs. We would like to highlight in gold any breeders with more than 2 champions. Here the conditional formula is $Number_of_Champions > 2 . We would also like to highlight breeders with 1 or 2 champion dogs in blue, and 0 champion dogs in brown. Click Add another rule to add more conditional styles. To add conditional formatting to rows, go to the ROW STYLE section of the creator panel under the Table > Widget tab, and click on Add conditional style . Order of Rules # Note that Grist applies the rules in order. Styles applied by later rules will override those applied by earlier rules. What would happen if we swapped the last two rules in the example above? Notice that Gen Hamamoto, who has 0 champion dogs, is not highlighted in brown. This is because after applying the second conditional style, $Number_of_Champions == 0 , Grist applied the third, $Number_of_Champions <= 2 , which applies to Gen Hamamoto as well and shades him blue.","title":"Conditional Formatting"},{"location":"conditional-formatting/#conditional-formatting","text":"Cell style can change based on conditional rules. Conditional rules are written as formulas. Conditional formatting can apply to an entire row or cells in a column. To add conditional formatting to a particular column, select the column, go to the CELL STYLE section of the creator panel under the Column tab, and click on Add conditional style . In this example, we have a list of dog breeders who have raised champion thoroughbreds. Let\u2019s apply conditional formatting to the Breeder column based on the number of champion dogs. We would like to highlight in gold any breeders with more than 2 champions. Here the conditional formula is $Number_of_Champions > 2 . We would also like to highlight breeders with 1 or 2 champion dogs in blue, and 0 champion dogs in brown. Click Add another rule to add more conditional styles. To add conditional formatting to rows, go to the ROW STYLE section of the creator panel under the Table > Widget tab, and click on Add conditional style .","title":"Conditional Formatting"},{"location":"conditional-formatting/#order-of-rules","text":"Note that Grist applies the rules in order. Styles applied by later rules will override those applied by earlier rules. What would happen if we swapped the last two rules in the example above? Notice that Gen Hamamoto, who has 0 champion dogs, is not highlighted in brown. This is because after applying the second conditional style, $Number_of_Champions == 0 , Grist applied the third, $Number_of_Champions <= 2 , which applies to Gen Hamamoto as well and shades him blue.","title":"Order of Rules"},{"location":"copying-docs/","text":"Copying Documents # It is sometimes useful to make a copy, or clone, of a Grist document. A few scenarios are described below. In all cases, you would start with the \u201cShare\u201d menu available from the top bar of an open document: Trying Out Changes # As your document grows in importance, it becomes riskier to make changes to its structure or logic. That\u2019s a good reason to work out such changes on a copy of the document, without fear of affecting the original. Open the Share menu and click the option \u201cWork on a Copy\u201d. You\u2019ll get an unsaved copy of your document. This copy is special in that it knows which original document it came from (you can see the original document ID included in its URL). You can experiment on this copy, making changes big or small, one or many. For those familiar with software development, this option is similar to a branch or a fork as used in version control systems like Git . When working on a copy, the Share menu has some new options. For example, you can view differences between the document copy and the original, using the \u201cCompare to Original\u201d menu item: New material will be highlighted in green, and old material in red. Once satisfied with your changes, click the option \u201cReplace Original\u201d. Your copy will replace the original document. Grist will warn you if the replacement risks overwriting any recent changes in the original. To discard your changes, simply go back to the original document using the \u201cReturn to Original\u201d option (or the Back button in your browser). Don\u2019t worry about cleaning up your copy. It does not count against any document limits, and will get cleaned up automatically if it has been unused for a while. You can also save your copy under a new name using the \u201cSave Copy\u201d option. Access to Unsaved Copies # When you create an experimental copy as described above, it gets a unique link. The copy isn\u2019t listed anywhere, so others will not find it unless you share this link. Anyone with a link to your copy and with access to the original document is allowed to view the copy, but you are the only user allowed to edit it. This means that you can share a link to your copy with others, who can review your changes. It also means that you can try out changes even if you cannot edit the original! You can then share a link to your copy with a collaborator who has edit access, who would be able to review your changes and apply them to the original document. Duplicating Documents # You can save your document under a new name using the \u201cDuplicate Document\u201d option in the Share menu. Clicking it opens a dialog: Type in the new name. If you have access to one or more team accounts, you may have a choice of a destination team and a destination workspace where to save the copy. Note that on a team site, you would not be able to save the document outside of the team site unless you have owner-level access to the document. Copying as a Template # If you mark the \u201cAs Template\u201d checkbox when saving a copy, you\u2019ll get a document which has all the structure, formulas, and layouts of the original, but none of the data. It makes it easy to use the existing structure for a new set of data. Copying for Backup Purposes # You can use the \u201cDuplicate Document\u201d option to save the current version of the document as a backup, perhaps appending today\u2019s date to the name of the copy. That said, Grist already makes automatic backups regularly, which may be sufficient for most backup needs. See Automatic Backups . Copying Public Examples # When you open a public example from the Examples & Templates page , it will open the example in fiddle mode . Fiddle mode is similar to working on a copy, as described above in Trying Out Changes . You may make changes, but they remain private to you. You can save a copy of the example under a new name using the \u201cSave Copy\u201d button or menu option. You can use the \u201cAs Template\u201d checkbox to discard the data of the example, keeping only its structure. This makes it easy to start using it for your own data.","title":"Copying documents"},{"location":"copying-docs/#copying-documents","text":"It is sometimes useful to make a copy, or clone, of a Grist document. A few scenarios are described below. In all cases, you would start with the \u201cShare\u201d menu available from the top bar of an open document:","title":"Copying Documents"},{"location":"copying-docs/#trying-out-changes","text":"As your document grows in importance, it becomes riskier to make changes to its structure or logic. That\u2019s a good reason to work out such changes on a copy of the document, without fear of affecting the original. Open the Share menu and click the option \u201cWork on a Copy\u201d. You\u2019ll get an unsaved copy of your document. This copy is special in that it knows which original document it came from (you can see the original document ID included in its URL). You can experiment on this copy, making changes big or small, one or many. For those familiar with software development, this option is similar to a branch or a fork as used in version control systems like Git . When working on a copy, the Share menu has some new options. For example, you can view differences between the document copy and the original, using the \u201cCompare to Original\u201d menu item: New material will be highlighted in green, and old material in red. Once satisfied with your changes, click the option \u201cReplace Original\u201d. Your copy will replace the original document. Grist will warn you if the replacement risks overwriting any recent changes in the original. To discard your changes, simply go back to the original document using the \u201cReturn to Original\u201d option (or the Back button in your browser). Don\u2019t worry about cleaning up your copy. It does not count against any document limits, and will get cleaned up automatically if it has been unused for a while. You can also save your copy under a new name using the \u201cSave Copy\u201d option.","title":"Trying Out Changes"},{"location":"copying-docs/#access-to-unsaved-copies","text":"When you create an experimental copy as described above, it gets a unique link. The copy isn\u2019t listed anywhere, so others will not find it unless you share this link. Anyone with a link to your copy and with access to the original document is allowed to view the copy, but you are the only user allowed to edit it. This means that you can share a link to your copy with others, who can review your changes. It also means that you can try out changes even if you cannot edit the original! You can then share a link to your copy with a collaborator who has edit access, who would be able to review your changes and apply them to the original document.","title":"Access to Unsaved Copies"},{"location":"copying-docs/#duplicating-documents","text":"You can save your document under a new name using the \u201cDuplicate Document\u201d option in the Share menu. Clicking it opens a dialog: Type in the new name. If you have access to one or more team accounts, you may have a choice of a destination team and a destination workspace where to save the copy. Note that on a team site, you would not be able to save the document outside of the team site unless you have owner-level access to the document.","title":"Duplicating Documents"},{"location":"copying-docs/#copying-as-a-template","text":"If you mark the \u201cAs Template\u201d checkbox when saving a copy, you\u2019ll get a document which has all the structure, formulas, and layouts of the original, but none of the data. It makes it easy to use the existing structure for a new set of data.","title":"Copying as a Template"},{"location":"copying-docs/#copying-for-backup-purposes","text":"You can use the \u201cDuplicate Document\u201d option to save the current version of the document as a backup, perhaps appending today\u2019s date to the name of the copy. That said, Grist already makes automatic backups regularly, which may be sufficient for most backup needs. See Automatic Backups .","title":"Copying for Backup Purposes"},{"location":"copying-docs/#copying-public-examples","text":"When you open a public example from the Examples & Templates page , it will open the example in fiddle mode . Fiddle mode is similar to working on a copy, as described above in Trying Out Changes . You may make changes, but they remain private to you. You can save a copy of the example under a new name using the \u201cSave Copy\u201d button or menu option. You can use the \u201cAs Template\u201d checkbox to discard the data of the example, keeping only its structure. This makes it easy to start using it for your own data.","title":"Copying Public Examples"},{"location":"creating-doc/","text":"Creating a document # To get started with Grist you\u2019ll first need to create a document. Each document can store different kinds of data, so think of it as a collection of related data. You can start from scratch or import existing data. Click the \u201cAdd New\u201d button on the home screen and choose either \u201cCreate empty document\u201d or \u201cImport document\u201d. You can import spreadsheets in Excel format, as well as comma-separated value files or CSVs. If you import a spreadsheet that has multiple tabs, each tab will become a separate table in Grist. Examples and templates # The \u201cExamples & Templates\u201d list on the Grist homepage includes a number of Grist documents that demonstrate Grist features and use cases. These are always accessible via a link on the bottom left of the home page. For each example, there is a corresponding How-To Tutorial in the Grist Help Center, which describes how to use it or how to build such a document from scratch. When you open an example, you can make changes to it. You are editing a copy, and your changes are private to you. You can save your changes by clicking the \u201cSave Copy\u201d button on the top of the page. You can also use any of the examples as a template for a new document, whch will include the structure, layout, and formulas of the example, but none of the sample data. Simply use the \u201cSave Copy\u201d button and check the \u201cAs Template\u201d checkbox in the dialog that opens. Importing more data # Once you\u2019ve created a document, you can import more data into it by opening the document and selecting \u201cAdd New\u201d and then one of the Import options. You can read more about importing data at Importing more data . Document settings # While a document is open, you can access the document\u2019s settings from the user menu or from the Tools menu in the left-hand navigation panel. There you can find the document\u2019s ID for API use, as well as set the default timezone, locale, and currency for the document.","title":"Creating a document"},{"location":"creating-doc/#creating-a-document","text":"To get started with Grist you\u2019ll first need to create a document. Each document can store different kinds of data, so think of it as a collection of related data. You can start from scratch or import existing data. Click the \u201cAdd New\u201d button on the home screen and choose either \u201cCreate empty document\u201d or \u201cImport document\u201d. You can import spreadsheets in Excel format, as well as comma-separated value files or CSVs. If you import a spreadsheet that has multiple tabs, each tab will become a separate table in Grist.","title":"Creating a document"},{"location":"creating-doc/#examples-and-templates","text":"The \u201cExamples & Templates\u201d list on the Grist homepage includes a number of Grist documents that demonstrate Grist features and use cases. These are always accessible via a link on the bottom left of the home page. For each example, there is a corresponding How-To Tutorial in the Grist Help Center, which describes how to use it or how to build such a document from scratch. When you open an example, you can make changes to it. You are editing a copy, and your changes are private to you. You can save your changes by clicking the \u201cSave Copy\u201d button on the top of the page. You can also use any of the examples as a template for a new document, whch will include the structure, layout, and formulas of the example, but none of the sample data. Simply use the \u201cSave Copy\u201d button and check the \u201cAs Template\u201d checkbox in the dialog that opens.","title":"Examples and templates"},{"location":"creating-doc/#importing-more-data","text":"Once you\u2019ve created a document, you can import more data into it by opening the document and selecting \u201cAdd New\u201d and then one of the Import options. You can read more about importing data at Importing more data .","title":"Importing more data"},{"location":"creating-doc/#document-settings","text":"While a document is open, you can access the document\u2019s settings from the user menu or from the Tools menu in the left-hand navigation panel. There you can find the document\u2019s ID for API use, as well as set the default timezone, locale, and currency for the document.","title":"Document settings"},{"location":"custom-layouts/","text":"Custom Layouts # You can easily add multiple widgets to one page, as described in Page widgets , and link them as described in Linking widgets . It is also easy to customize their arrangement. Move the mouse cursor over the title of the widget. A small drag icon will appear over to the left of the title. When you press this icon, you can drag the entire widget to a different place on the screen. As you move it close to the edge of the screen, or the edge of another widget, you\u2019ll see a dashed outline \u2013 sometimes more than one \u2013 where the widget can be dropped. Release the mouse to reposition this widget. To resize widgets, move the mouse cursor between two widgets until a dashed line appear. Drag this dashed line to resize. To expand a widget, click the expand icon at the upper-right of the widget. This will open a full-page view of the widget. To collapse a widget, click and drag a widget to the widget tray at the top of the page. When you click on a collapsed widget, it opens in a full-page view. To restore it to the main page, just drag the collapsed widget to the desired location. Layout recommendations # While there is no limit to how complicated a layout you can create, you should aim for simple layouts that will be easy to use for your users (even when you are the only user!) One rule of thumb is that a widget controlled by another \u201cselector\u201d widget (see Linking widgets ) should be to the right or below it. Here are some common layouts. Layout: List and detail # The most common one is to have a list of items on the left, with one or more widgets on the right providing more information. For instance, the Lightweight CRM example includes a list of people on the left, with a person\u2019s card and a table of related interactions to the right of the list. In this usage, you might want to include in the list only the minimal information you need, perhaps only a contact\u2019s name. If your table has many columns, a quick way to leave only a few is via the widget options in the right-side panel. In the table widget, click the three-dot menu on the top right, and select \u201cWidget options\u201d. You\u2019ll see a list of \u201cVisible Columns\u201d. Click \u201cSelect all\u201d link on top of that list: Now uncheck the few fields you want to keep, and click \u201cHide columns\u201d to hide the rest. Layout: Spreadsheet plus # Sometimes a wide spreadsheet with many columns is convenient. If you\u2019d like to see more info associated with the rows of this spreadsheet, you can add widgets below it. These could be details linked to the spreadsheet, or summary tables that show totals or other global info. For instance, here is a possible layout based on the Lightweight CRM example. It shows contacts as a wide spreadsheet, and below that includes sections with an overall summary, and interactions for the selected contact. Layout: Summary and details # Sometimes it\u2019s useful to divide up a large dataset into subsets. For instance, you might have credit card transactions, and want a way to view them one month at a time. To do it, you\u2019ll use a \u201cMonth\u201d column, creating one with a formula if needed. Then create a summary table grouped by \u201cMonth\u201d (see Summary tables , and link the table of transactions to it. We can then select a month and see a spreadsheet of only the transactions in that month. Layout: Charts dashboard # If you have many charts, you can just lay them out in a grid to create a top-level dashboard. For dynamic charts in which data is selected by another table, a layout like List-and-detail above would work well. One tip is to include both a Table widget and a Chart widget, configured and linked the same way, and differing only in the widget type: Having a table alongside the chart can be a useful reference, as well as provide more context to what is visible in the chart.","title":"Custom layouts"},{"location":"custom-layouts/#custom-layouts","text":"You can easily add multiple widgets to one page, as described in Page widgets , and link them as described in Linking widgets . It is also easy to customize their arrangement. Move the mouse cursor over the title of the widget. A small drag icon will appear over to the left of the title. When you press this icon, you can drag the entire widget to a different place on the screen. As you move it close to the edge of the screen, or the edge of another widget, you\u2019ll see a dashed outline \u2013 sometimes more than one \u2013 where the widget can be dropped. Release the mouse to reposition this widget. To resize widgets, move the mouse cursor between two widgets until a dashed line appear. Drag this dashed line to resize. To expand a widget, click the expand icon at the upper-right of the widget. This will open a full-page view of the widget. To collapse a widget, click and drag a widget to the widget tray at the top of the page. When you click on a collapsed widget, it opens in a full-page view. To restore it to the main page, just drag the collapsed widget to the desired location.","title":"Custom Layouts"},{"location":"custom-layouts/#layout-recommendations","text":"While there is no limit to how complicated a layout you can create, you should aim for simple layouts that will be easy to use for your users (even when you are the only user!) One rule of thumb is that a widget controlled by another \u201cselector\u201d widget (see Linking widgets ) should be to the right or below it. Here are some common layouts.","title":"Layout recommendations"},{"location":"custom-layouts/#layout-list-and-detail","text":"The most common one is to have a list of items on the left, with one or more widgets on the right providing more information. For instance, the Lightweight CRM example includes a list of people on the left, with a person\u2019s card and a table of related interactions to the right of the list. In this usage, you might want to include in the list only the minimal information you need, perhaps only a contact\u2019s name. If your table has many columns, a quick way to leave only a few is via the widget options in the right-side panel. In the table widget, click the three-dot menu on the top right, and select \u201cWidget options\u201d. You\u2019ll see a list of \u201cVisible Columns\u201d. Click \u201cSelect all\u201d link on top of that list: Now uncheck the few fields you want to keep, and click \u201cHide columns\u201d to hide the rest.","title":"Layout: List and detail"},{"location":"custom-layouts/#layout-spreadsheet-plus","text":"Sometimes a wide spreadsheet with many columns is convenient. If you\u2019d like to see more info associated with the rows of this spreadsheet, you can add widgets below it. These could be details linked to the spreadsheet, or summary tables that show totals or other global info. For instance, here is a possible layout based on the Lightweight CRM example. It shows contacts as a wide spreadsheet, and below that includes sections with an overall summary, and interactions for the selected contact.","title":"Layout: Spreadsheet plus"},{"location":"custom-layouts/#layout-summary-and-details","text":"Sometimes it\u2019s useful to divide up a large dataset into subsets. For instance, you might have credit card transactions, and want a way to view them one month at a time. To do it, you\u2019ll use a \u201cMonth\u201d column, creating one with a formula if needed. Then create a summary table grouped by \u201cMonth\u201d (see Summary tables , and link the table of transactions to it. We can then select a month and see a spreadsheet of only the transactions in that month.","title":"Layout: Summary and details"},{"location":"custom-layouts/#layout-charts-dashboard","text":"If you have many charts, you can just lay them out in a grid to create a top-level dashboard. For dynamic charts in which data is selected by another table, a layout like List-and-detail above would work well. One tip is to include both a Table widget and a Chart widget, configured and linked the same way, and differing only in the widget type: Having a table alongside the chart can be a useful reference, as well as provide more context to what is visible in the chart.","title":"Layout: Charts dashboard"},{"location":"data-security/","text":"Data Security # Grist is available as a hosted service (\u201cGrist SaaS\u201d) running on infrastructure managed by Grist Labs. It can also be installed on your own infrastructure (\u201cSelf-Managed Grist\u201d). In either case, we take measures to secure your data that you should know about. Grist SaaS # Our general privacy policy and terms are at https://www.getgrist.com/privacy and https://www.getgrist.com/terms . In addition, here is a summary of specific measures that relate to Grist documents that we host on your behalf: Grist servers operate in the Amazon Web Services (AWS) cloud infrastructure, in the United States. AWS S3 is used for long-term storage, and stores documents in encrypted form. Data is stored in the United States. Grist employees never look at your data and cannot open your documents. The one exception is if you choose to share a document with customer support in order to get help with an issue. When being operated on, your data will by necessity exist in unencrypted form in some of Grist\u2019s internal systems. Only select key employees have full access to these systems, and policy prohibits them from looking inside documents. Secure HTTPS is used for all access to Grist via public internet (both website and API calls). Regular backups of Grist documents are made, and are stored in encrypted form. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. If you delete a document, it will persist for 30 days in a Trash folder under your control. At any time a \u201cDelete forever\u201d option can be used to immediately purge a document in this folder. All automatic backups are purged along with the document. The hosted Grist product has not at this time gone through certification or auditing for SOC 2, ISO 27001, HIPAA, or GDPR compliance. If you need specific documentation, please contact customer support. You can also nudge us to prioritize certification over feature development at this issue . Self-Managed Grist # For Self-Managed Grist, you are in complete control of where servers operate and where data is stored. Here are some security considerations to bear in mind: Grist software is distributed via the gristlabs organization on github and docker hub . Please exercise diligence if accessing software elsewhere, since the software you install will have full access to your data. Grist documents support powerful Python formulas. Please pay attention to instructions for configuring sandboxing if your team may be working with untrusted documents. Grist by default is welcoming to anonymous users, allowing them to create and edit their own documents. You may wish to configure a stricter arrangement . Grist does not make external services mandatory, since that would introduce unnecessary obstacles in some scenarios. For example, an individual editing a Grist document offline on their own desktop shouldn\u2019t need to install a PostgreSQL database first. But it is important to evaluate what you need in your situation rather than simply sticking with the defaults. Please read about the data Grist stores and your options for where to store it. It is important to keep your Grist installation up to date .","title":"Data Security"},{"location":"data-security/#data-security","text":"Grist is available as a hosted service (\u201cGrist SaaS\u201d) running on infrastructure managed by Grist Labs. It can also be installed on your own infrastructure (\u201cSelf-Managed Grist\u201d). In either case, we take measures to secure your data that you should know about.","title":"Data Security"},{"location":"data-security/#grist-saas","text":"Our general privacy policy and terms are at https://www.getgrist.com/privacy and https://www.getgrist.com/terms . In addition, here is a summary of specific measures that relate to Grist documents that we host on your behalf: Grist servers operate in the Amazon Web Services (AWS) cloud infrastructure, in the United States. AWS S3 is used for long-term storage, and stores documents in encrypted form. Data is stored in the United States. Grist employees never look at your data and cannot open your documents. The one exception is if you choose to share a document with customer support in order to get help with an issue. When being operated on, your data will by necessity exist in unencrypted form in some of Grist\u2019s internal systems. Only select key employees have full access to these systems, and policy prohibits them from looking inside documents. Secure HTTPS is used for all access to Grist via public internet (both website and API calls). Regular backups of Grist documents are made, and are stored in encrypted form. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. If you delete a document, it will persist for 30 days in a Trash folder under your control. At any time a \u201cDelete forever\u201d option can be used to immediately purge a document in this folder. All automatic backups are purged along with the document. The hosted Grist product has not at this time gone through certification or auditing for SOC 2, ISO 27001, HIPAA, or GDPR compliance. If you need specific documentation, please contact customer support. You can also nudge us to prioritize certification over feature development at this issue .","title":"Grist SaaS"},{"location":"data-security/#self-managed-grist","text":"For Self-Managed Grist, you are in complete control of where servers operate and where data is stored. Here are some security considerations to bear in mind: Grist software is distributed via the gristlabs organization on github and docker hub . Please exercise diligence if accessing software elsewhere, since the software you install will have full access to your data. Grist documents support powerful Python formulas. Please pay attention to instructions for configuring sandboxing if your team may be working with untrusted documents. Grist by default is welcoming to anonymous users, allowing them to create and edit their own documents. You may wish to configure a stricter arrangement . Grist does not make external services mandatory, since that would introduce unnecessary obstacles in some scenarios. For example, an individual editing a Grist document offline on their own desktop shouldn\u2019t need to install a PostgreSQL database first. But it is important to evaluate what you need in your situation rather than simply sticking with the defaults. Please read about the data Grist stores and your options for where to store it. It is important to keep your Grist installation up to date .","title":"Self-Managed Grist"},{"location":"dates/","text":"Overview # Grist expresses dates and times in two ways. The first is the Date column type, which represents a calendar date, with no time of day, and not associated with any particular timezone. The second is the DateTime column type, which represents a calendar date with a time of day which can be linked with a timezone. The Date and DateTime column types support different formatting options. When a column is set to be a Date or a DateTime , a date-picker widget will let you select the date on a calendar when editing a cell. When working with dates in formulas, the dates are Python datetime objects . That allows you to do some powerful things, but can be unexpected if you\u2019re not familiar with them. Making a date/time column # For a general introduction to setting the type of columns, see Columns and data types . To tell Grist that you intend to enter only date/times in a column, over on the header for the column, find the drop-down, and select \u201cColumn Options\u201d. Then in the side panel that opens on the right, pick \u201cDate\u201d from the \u201cColumn Type\u201d drop-down. Or, if you want dates with times, pick \u201cDateTime\u201d. Then you can choose your preferred date/time format. For the \u201cDateTime\u201d type, you can also choose the timezone. When you convert a column from another type, such as \u201cText\u201d, you\u2019ll see a preview of the conversion results, and will need to click \u201cApply\u201d to complete conversion. You can come back and change settings at any time. Now when you edit a cell in this column, you will have help for selecting dates and times. Inserting the current date # You can insert the current date in a cell using \u2318 + ; (semicolon) (Mac) or Ctrl + ; (Windows). You can insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows). When editing a date cell, the date entry widget has a \u201ctoday\u201d button for today\u2019s date. Parsing dates from strings # The DATEVALUE function converts a string that represents a date into a datetime object. It\u2019s simple to use and it will auto-detect different date formats: You can also use Python\u2019s datetime library, which provides two helpful functions: strptime() and strftime() . For example, let\u2019s say you have a table of movie sequels and their release dates (as strings). You\u2019d like to parse out the actual date to be able to sort the table properly. Here\u2019s how you would do that: First line imports the datetime library The second line splits the string into two parts and returns the second part (Python arrays are zero-based). The third line uses Python\u2019s strptime function to parse the date (e.g. \u201cMay 19, 1999\u201d) into a datetime object. The first parameter to the function is the string to parse, the second parameter is the date format that the string is in. Take a look at the format options to see if the example format string %B %d, %Y makes sense. (Note: You could\u2019ve also used DATEVALUE(d) to achieve the same result.) The result has a true date column and can now be properly sorted chronologically, with \u201cA New Hope\u201d at the top. For historical reasons, the first Star Wars movie is considered to be Episode 4. And, because the column type is selected as a date, you can use the \u201cDate Format\u201d in \u201cColumn Options\u201d to select the format in which to display the date. For some situations, you may wish to use the dateutil python library. For example, if you live in an area where dates typically start with the day and then the month, you could use this formula: import dateutil dateutil.parser.parse($date_text, dayfirst=True) Date arithmetic # Once you have a proper date column, often you\u2019ll want to do date arithmetic such as calculating the difference between two dates. The simplest way to do this is to use the DATEDIF function which takes two dates and the unit of information to return (Days, Months, or Years). You could also use the minus sign to subtract two dates, but you might be surprised at the result: This happens because subtracting two datetime objects as we did in the example above, results in a datetime.timedelta object which represents, \u201cA duration expressing the difference between two date, time, or datetime instances to microsecond resolution.\u201d In Grist (and Python) you have to be more specific above how you want to display the date difference. For example, to get the number of days from the returned timedelta object, use its .days property: If you want weeks or years, just divide by 7 or by 365. (Divide by 7.0 or 365.0 to include a fractional part in the result.) If you want hours, multiply by 24. You can also use specific functions to get what you want. For example, DAYS is a common function in spreadsheet apps that returns the difference between two dates: DAYS($Last_day, $First_day) Excel/Sheets formulas Grist supports many other common functions from other spreadsheet apps, including DATEADD , DATEDIF , DATEVALUE , MONTH , HOUR , and many more . Getting a part of the date # You\u2019ve seen how to parse the date, display it in different formats, and do date arithmetic. But what if you want to get more information about a specific date, such as getting its day of the week? One option is to use the WEEKDAY function, which behaves as it does in Excel, returning 1-7 for Sunday-Saturday. Alternatively, we can use the strftime function: Yet another option would be to reformat the date using Date Format in Column Options (see the date formatting reference ). Time zones # Values in DateTime columns represent moments in time. The same moment will look different in different timezones. In Grist, the timezone is set on each DateTime column. For instance, if the timezone is set to \u201cAmerica/New_York\u201d, it will show the values in New York timezone to collaborators anywhere in the world. A Grist document has a global timezone setting, which serves as the default timezone for when you create a new column of type DateTime . This global timezone is set to your local timezone when you first create a document. You can see or change it by clicking on your profile picture or icon, and selecting \u201cDocument Settings\u201d. If you insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows) into a DateTime column, it will be inserted as a true timezone-aware timestamp, and shown with the timezone set for that column. If you do the same in a Text column, the date/time will be inserted as the text appropriate for the document\u2019s global timezone setting. Similarly, inserting the current date into a Date column will produce the current date according to the document\u2019s timezone. Additional resources # Python cheatsheet for strftime , for using with strftime() and strptime() in formulas. Date formatting cheatsheet , for specifying the date/time format in column settings. dateutil library , extensions to the Python standard datetime module.","title":"Working with dates"},{"location":"dates/#overview","text":"Grist expresses dates and times in two ways. The first is the Date column type, which represents a calendar date, with no time of day, and not associated with any particular timezone. The second is the DateTime column type, which represents a calendar date with a time of day which can be linked with a timezone. The Date and DateTime column types support different formatting options. When a column is set to be a Date or a DateTime , a date-picker widget will let you select the date on a calendar when editing a cell. When working with dates in formulas, the dates are Python datetime objects . That allows you to do some powerful things, but can be unexpected if you\u2019re not familiar with them.","title":"Overview"},{"location":"dates/#making-a-datetime-column","text":"For a general introduction to setting the type of columns, see Columns and data types . To tell Grist that you intend to enter only date/times in a column, over on the header for the column, find the drop-down, and select \u201cColumn Options\u201d. Then in the side panel that opens on the right, pick \u201cDate\u201d from the \u201cColumn Type\u201d drop-down. Or, if you want dates with times, pick \u201cDateTime\u201d. Then you can choose your preferred date/time format. For the \u201cDateTime\u201d type, you can also choose the timezone. When you convert a column from another type, such as \u201cText\u201d, you\u2019ll see a preview of the conversion results, and will need to click \u201cApply\u201d to complete conversion. You can come back and change settings at any time. Now when you edit a cell in this column, you will have help for selecting dates and times.","title":"Making a date/time column"},{"location":"dates/#inserting-the-current-date","text":"You can insert the current date in a cell using \u2318 + ; (semicolon) (Mac) or Ctrl + ; (Windows). You can insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows). When editing a date cell, the date entry widget has a \u201ctoday\u201d button for today\u2019s date.","title":"Inserting the current date"},{"location":"dates/#parsing-dates-from-strings","text":"The DATEVALUE function converts a string that represents a date into a datetime object. It\u2019s simple to use and it will auto-detect different date formats: You can also use Python\u2019s datetime library, which provides two helpful functions: strptime() and strftime() . For example, let\u2019s say you have a table of movie sequels and their release dates (as strings). You\u2019d like to parse out the actual date to be able to sort the table properly. Here\u2019s how you would do that: First line imports the datetime library The second line splits the string into two parts and returns the second part (Python arrays are zero-based). The third line uses Python\u2019s strptime function to parse the date (e.g. \u201cMay 19, 1999\u201d) into a datetime object. The first parameter to the function is the string to parse, the second parameter is the date format that the string is in. Take a look at the format options to see if the example format string %B %d, %Y makes sense. (Note: You could\u2019ve also used DATEVALUE(d) to achieve the same result.) The result has a true date column and can now be properly sorted chronologically, with \u201cA New Hope\u201d at the top. For historical reasons, the first Star Wars movie is considered to be Episode 4. And, because the column type is selected as a date, you can use the \u201cDate Format\u201d in \u201cColumn Options\u201d to select the format in which to display the date. For some situations, you may wish to use the dateutil python library. For example, if you live in an area where dates typically start with the day and then the month, you could use this formula: import dateutil dateutil.parser.parse($date_text, dayfirst=True)","title":"Parsing dates from strings"},{"location":"dates/#date-arithmetic","text":"Once you have a proper date column, often you\u2019ll want to do date arithmetic such as calculating the difference between two dates. The simplest way to do this is to use the DATEDIF function which takes two dates and the unit of information to return (Days, Months, or Years). You could also use the minus sign to subtract two dates, but you might be surprised at the result: This happens because subtracting two datetime objects as we did in the example above, results in a datetime.timedelta object which represents, \u201cA duration expressing the difference between two date, time, or datetime instances to microsecond resolution.\u201d In Grist (and Python) you have to be more specific above how you want to display the date difference. For example, to get the number of days from the returned timedelta object, use its .days property: If you want weeks or years, just divide by 7 or by 365. (Divide by 7.0 or 365.0 to include a fractional part in the result.) If you want hours, multiply by 24. You can also use specific functions to get what you want. For example, DAYS is a common function in spreadsheet apps that returns the difference between two dates: DAYS($Last_day, $First_day) Excel/Sheets formulas Grist supports many other common functions from other spreadsheet apps, including DATEADD , DATEDIF , DATEVALUE , MONTH , HOUR , and many more .","title":"Date arithmetic"},{"location":"dates/#getting-a-part-of-the-date","text":"You\u2019ve seen how to parse the date, display it in different formats, and do date arithmetic. But what if you want to get more information about a specific date, such as getting its day of the week? One option is to use the WEEKDAY function, which behaves as it does in Excel, returning 1-7 for Sunday-Saturday. Alternatively, we can use the strftime function: Yet another option would be to reformat the date using Date Format in Column Options (see the date formatting reference ).","title":"Getting a part of the date"},{"location":"dates/#time-zones","text":"Values in DateTime columns represent moments in time. The same moment will look different in different timezones. In Grist, the timezone is set on each DateTime column. For instance, if the timezone is set to \u201cAmerica/New_York\u201d, it will show the values in New York timezone to collaborators anywhere in the world. A Grist document has a global timezone setting, which serves as the default timezone for when you create a new column of type DateTime . This global timezone is set to your local timezone when you first create a document. You can see or change it by clicking on your profile picture or icon, and selecting \u201cDocument Settings\u201d. If you insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows) into a DateTime column, it will be inserted as a true timezone-aware timestamp, and shown with the timezone set for that column. If you do the same in a Text column, the date/time will be inserted as the text appropriate for the document\u2019s global timezone setting. Similarly, inserting the current date into a Date column will produce the current date according to the document\u2019s timezone.","title":"Time zones"},{"location":"dates/#additional-resources","text":"Python cheatsheet for strftime , for using with strftime() and strptime() in formulas. Date formatting cheatsheet , for specifying the date/time format in column settings. dateutil library , extensions to the Python standard datetime module.","title":"Additional resources"},{"location":"document-history/","text":"Document history # To access information about a document\u2019s history, click \u201cDocument History\u201d in the left panel. The right panel will then offer two tabs, \u201cActivity\u201d and \u201cSnapshots\u201d. Snapshots # Grist automatically saves complete backups of your documents as you work on them, making them available on the Snapshots tab. Read Automatic Backups for details on using this tab to examine, compare, and restore from older versions of a document. Activity # The Activity tab lists all recent changes for the table associated with the widget you\u2019re currently looking at. Select the \u201cAll tables\u201d checkbox to see recent changes in the document regardless of table. Changes are shown as small table extracts. The cells are clickable, and bring you to the current version of the cell (if it still exists) in the \u201cprimary\u201d widget for the table containing it (this is typically the first widget created showing the table).","title":"Document history"},{"location":"document-history/#document-history","text":"To access information about a document\u2019s history, click \u201cDocument History\u201d in the left panel. The right panel will then offer two tabs, \u201cActivity\u201d and \u201cSnapshots\u201d.","title":"Document history"},{"location":"document-history/#snapshots","text":"Grist automatically saves complete backups of your documents as you work on them, making them available on the Snapshots tab. Read Automatic Backups for details on using this tab to examine, compare, and restore from older versions of a document.","title":"Snapshots"},{"location":"document-history/#activity","text":"The Activity tab lists all recent changes for the table associated with the widget you\u2019re currently looking at. Select the \u201cAll tables\u201d checkbox to see recent changes in the document regardless of table. Changes are shown as small table extracts. The cells are clickable, and bring you to the current version of the cell (if it still exists) in the \u201cprimary\u201d widget for the table containing it (this is typically the first widget created showing the table).","title":"Activity"},{"location":"embedding/","text":"Embedding Grist # Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view: Parameters # Read-Only vs. Editable # Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing . Appearance # Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Embedding"},{"location":"embedding/#embedding-grist","text":"Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view:","title":"Embedding Grist"},{"location":"embedding/#parameters","text":"","title":"Parameters"},{"location":"embedding/#read-only-vs-editable","text":"Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing .","title":"Read-Only vs. Editable"},{"location":"embedding/#appearance","text":"Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Appearance"},{"location":"enter-data/","text":"Entering data # A spreadsheet-like grid is a great way to see data. In Grist, this is the view offered by the default page widget called \u201cTable\u201d. As in a spreadsheet, you can use the mouse or arrow keys to move around the cells of a table. To start entering data into a selecte cell, either start typing, hit Enter , or double-click the cell. Editing cells # While editing a cell, several keys are special: Escape cancels the operation and restores the previous value in the cell. Tab , Shift + Tab saves your entry and moves your cursor to the next or previous cell. Enter saves your entry and moves your cursor to the next row. Shift + Enter adds a newline inside your cell. Copying and pasting # You can copy data from Grist or paste data into it. If the pasted range is longer than the available records, new records will be added. Note that Grist does not create new columns automatically. If the pasted data has more columns than the grid displays, extra columns will be omitted. Data entry widgets # In Grist, columns have types. In addition to typing in values, many column types offer specialized widgets for entering data more conveniently. Here are some of the most useful ones: Toggle . A \u201cToggle\u201d column shows True/False values, and can show them as a \u201cCheckbox\u201d or as a \u201cSwitch\u201d widget, which you can select in the column options. You can toggle a value in such a cell by clicking the check mark or the switch, or by hitting Space . Date and DateTime . Hitting Enter on such a cell will open a calendar to pick a date. Choice and Reference . Typing into a cell of one of these types will produce an auto-complete dropdown menu. Linking to cells # You can make a sharable link to a cell by pressing \u2318 \u21e7 A (Mac) or Ctrl + Shift + A (Windows) while the cell is selected. This option is also available via the row menu as \u201cCopy anchor link.\u201d The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document.","title":"Entering data"},{"location":"enter-data/#entering-data","text":"A spreadsheet-like grid is a great way to see data. In Grist, this is the view offered by the default page widget called \u201cTable\u201d. As in a spreadsheet, you can use the mouse or arrow keys to move around the cells of a table. To start entering data into a selecte cell, either start typing, hit Enter , or double-click the cell.","title":"Entering data"},{"location":"enter-data/#editing-cells","text":"While editing a cell, several keys are special: Escape cancels the operation and restores the previous value in the cell. Tab , Shift + Tab saves your entry and moves your cursor to the next or previous cell. Enter saves your entry and moves your cursor to the next row. Shift + Enter adds a newline inside your cell.","title":"Editing cells"},{"location":"enter-data/#copying-and-pasting","text":"You can copy data from Grist or paste data into it. If the pasted range is longer than the available records, new records will be added. Note that Grist does not create new columns automatically. If the pasted data has more columns than the grid displays, extra columns will be omitted.","title":"Copying and pasting"},{"location":"enter-data/#data-entry-widgets","text":"In Grist, columns have types. In addition to typing in values, many column types offer specialized widgets for entering data more conveniently. Here are some of the most useful ones: Toggle . A \u201cToggle\u201d column shows True/False values, and can show them as a \u201cCheckbox\u201d or as a \u201cSwitch\u201d widget, which you can select in the column options. You can toggle a value in such a cell by clicking the check mark or the switch, or by hitting Space . Date and DateTime . Hitting Enter on such a cell will open a calendar to pick a date. Choice and Reference . Typing into a cell of one of these types will produce an auto-complete dropdown menu.","title":"Data entry widgets"},{"location":"enter-data/#linking-to-cells","text":"You can make a sharable link to a cell by pressing \u2318 \u21e7 A (Mac) or Ctrl + Shift + A (Windows) while the cell is selected. This option is also available via the row menu as \u201cCopy anchor link.\u201d The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document.","title":"Linking to cells"},{"location":"examples/","text":"More Examples # Here we will post useful examples of what you can do with Grist, sometimes with ready-to-use templates. Slicing and Dicing Credit Card Expenses : Grist offers a handy way to explore your credit card transactions quickly. American Express card members can use the ready-made template in the example. Book Lists with Library and Store Look-ups : Grist\u2019s link handling and formulas can help you with your book club, by adding automatic links to libraries, stores, review sites, and other references. The same ideas could work with Ikea furniture, food orders, whatever you like. Prepare Emails using Formulas : Turn cells into links that open an email program to compose a new message, and prefill its recipients, subject, and more. Preparing Invoices : Preview printable invoices side by side with your client data. Tracking Payroll : Keep track of employee rates, roles, and hours conveniently, with up-to-date reliable payroll summaries, and detailed history. Print Mailing Labels : Print mailing labels easily. A custom widget supporting popular label sizes makes it quick to generate labels and print from Grist. Treasure Hunt : Plan a treasure hunt without losing track. Brainstorm clues, then assemble them into a consistent trail. Map : Show locations listed in a table on a map. Task Management for Teams : This glorified To-Do list \u2013 similar to the one we use internally at Grist labs \u2013 works well to manage a team\u2019s tasks. Lead List : A very simple list of leads, assigned to individuals to follow up, with control of assignments reserved for document owners. Understanding Link Keys : Learn how to create unique links that share limited views of your Grist document. Reference Columns Guide : Mastering Reference Columns in 3 Steps Summary Tables Guide : Master Summary Tables with 2 Concepts, using the example of a timesheet tracker, with detailed account tracking. Auto Time and User Stamps : Learn to create columns that stamp the time or a user\u2019s name to a record when it is created or updated. Access Rules to Restrict Duplicate Records : Learn to how to create a condition in Access Rules to restrict a duplicate record from being created. Creating Proposals & Contracts : Preview printable forms side by side with your project data. Have something to share? # Have your own template to share with the world? Email us at support@getgrist.com , and we may feature it in a future post.","title":"More examples"},{"location":"examples/#more-examples","text":"Here we will post useful examples of what you can do with Grist, sometimes with ready-to-use templates. Slicing and Dicing Credit Card Expenses : Grist offers a handy way to explore your credit card transactions quickly. American Express card members can use the ready-made template in the example. Book Lists with Library and Store Look-ups : Grist\u2019s link handling and formulas can help you with your book club, by adding automatic links to libraries, stores, review sites, and other references. The same ideas could work with Ikea furniture, food orders, whatever you like. Prepare Emails using Formulas : Turn cells into links that open an email program to compose a new message, and prefill its recipients, subject, and more. Preparing Invoices : Preview printable invoices side by side with your client data. Tracking Payroll : Keep track of employee rates, roles, and hours conveniently, with up-to-date reliable payroll summaries, and detailed history. Print Mailing Labels : Print mailing labels easily. A custom widget supporting popular label sizes makes it quick to generate labels and print from Grist. Treasure Hunt : Plan a treasure hunt without losing track. Brainstorm clues, then assemble them into a consistent trail. Map : Show locations listed in a table on a map. Task Management for Teams : This glorified To-Do list \u2013 similar to the one we use internally at Grist labs \u2013 works well to manage a team\u2019s tasks. Lead List : A very simple list of leads, assigned to individuals to follow up, with control of assignments reserved for document owners. Understanding Link Keys : Learn how to create unique links that share limited views of your Grist document. Reference Columns Guide : Mastering Reference Columns in 3 Steps Summary Tables Guide : Master Summary Tables with 2 Concepts, using the example of a timesheet tracker, with detailed account tracking. Auto Time and User Stamps : Learn to create columns that stamp the time or a user\u2019s name to a record when it is created or updated. Access Rules to Restrict Duplicate Records : Learn to how to create a condition in Access Rules to restrict a duplicate record from being created. Creating Proposals & Contracts : Preview printable forms side by side with your project data.","title":"More Examples"},{"location":"examples/#have-something-to-share","text":"Have your own template to share with the world? Email us at support@getgrist.com , and we may feature it in a future post.","title":"Have something to share?"},{"location":"exports/","text":"Exporting # Exporting a table # If you want to export a table to another spreadsheet or database, you can export that table as either an XLSX file or a CSV, a common interchange format for data. To do this, open your document to the desired table or widget. Then click the three dot menu in the top right of the widget. Select either \u201cDownload as CSV\u201d or \u201cDownload as XLSX\u201d. Your browser will then download a file containing a header row naming your columns, excluding any hidden columns or filtered-out rows, followed by all the rows of data visible in the table. Exporting a document # If you want to export all tables to Excel format, click the sharing icon ( ) on the top right of the screen and select \u201cExport XLSX\u201d. Your browser will then download an Excel file, where each table is a separate sheet containing all rows, without any filters applied. To use this option you need to have full read access for all tables in a document. A note about \u201cExport CSV\u201d in the sharing menu. When on a page with multiple page widgets, \u201cExport CSV\u201d will export only the data in the currently-selected widget. To export all your data, use \u201cExport XLSX\u201d or the \u201cDownload\u201d option which is explained below in backing up an entire document . Sending to Google Drive # If you want to export a document to Google Drive as a Google Sheet file, click the sharing icon ( ) on the top right of the screen and select \u201cSend to Google Drive\u201d. Grist will first ask you to log in to your Google Account (or use an account you already signed in) and then for permission to create a file in your Google Drive. Grist will be able to create new files and manage them but will not be able to access any other files in your drive. Once you accept the permission request, Grist will export your document to an Excel file and then save it in your Google Drive as a Google Sheet file. To use this option, you need to have full read access for all tables in a document. Backing up an entire document # Grist makes regular backups of documents automatically, as described in Automatic Backups . You can also make manual backups by saving copies of documents in your Grist account. In addition, Grist documents can be downloaded in their entirety as an SQLite database file with a .grist extension. SQLite is a popular database format. The downloaded file will contain all your tabular data, any attached files within those tables, metadata about your tables, pages, and widgets, and a history of recent modifications of the document. It will not contain information about who the document is shared with. To download a Grist document, click the sharing icon ( ) on the top right of the screen, and select \u201cDownload\u201d. Restoring from backup # A downloaded .grist file can be uploaded again to provide an exact copy of the original. To upload the file, open the team or personal site where you want to place it, and optionally select also a workspace. Then click on \u201cAdd New\u201d in the top left, and select \u201cImport document\u201d. You may also import CSV and Excel files as new Grist documents this way.","title":"Exports & backups"},{"location":"exports/#exporting","text":"","title":"Exporting"},{"location":"exports/#exporting-a-table","text":"If you want to export a table to another spreadsheet or database, you can export that table as either an XLSX file or a CSV, a common interchange format for data. To do this, open your document to the desired table or widget. Then click the three dot menu in the top right of the widget. Select either \u201cDownload as CSV\u201d or \u201cDownload as XLSX\u201d. Your browser will then download a file containing a header row naming your columns, excluding any hidden columns or filtered-out rows, followed by all the rows of data visible in the table.","title":"Exporting a table"},{"location":"exports/#exporting-a-document","text":"If you want to export all tables to Excel format, click the sharing icon ( ) on the top right of the screen and select \u201cExport XLSX\u201d. Your browser will then download an Excel file, where each table is a separate sheet containing all rows, without any filters applied. To use this option you need to have full read access for all tables in a document. A note about \u201cExport CSV\u201d in the sharing menu. When on a page with multiple page widgets, \u201cExport CSV\u201d will export only the data in the currently-selected widget. To export all your data, use \u201cExport XLSX\u201d or the \u201cDownload\u201d option which is explained below in backing up an entire document .","title":"Exporting a document"},{"location":"exports/#sending-to-google-drive","text":"If you want to export a document to Google Drive as a Google Sheet file, click the sharing icon ( ) on the top right of the screen and select \u201cSend to Google Drive\u201d. Grist will first ask you to log in to your Google Account (or use an account you already signed in) and then for permission to create a file in your Google Drive. Grist will be able to create new files and manage them but will not be able to access any other files in your drive. Once you accept the permission request, Grist will export your document to an Excel file and then save it in your Google Drive as a Google Sheet file. To use this option, you need to have full read access for all tables in a document.","title":"Sending to Google Drive"},{"location":"exports/#backing-up-an-entire-document","text":"Grist makes regular backups of documents automatically, as described in Automatic Backups . You can also make manual backups by saving copies of documents in your Grist account. In addition, Grist documents can be downloaded in their entirety as an SQLite database file with a .grist extension. SQLite is a popular database format. The downloaded file will contain all your tabular data, any attached files within those tables, metadata about your tables, pages, and widgets, and a history of recent modifications of the document. It will not contain information about who the document is shared with. To download a Grist document, click the sharing icon ( ) on the top right of the screen, and select \u201cDownload\u201d.","title":"Backing up an entire document"},{"location":"exports/#restoring-from-backup","text":"A downloaded .grist file can be uploaded again to provide an exact copy of the original. To upload the file, open the team or personal site where you want to place it, and optionally select also a workspace. Then click on \u201cAdd New\u201d in the top left, and select \u201cImport document\u201d. You may also import CSV and Excel files as new Grist documents this way.","title":"Restoring from backup"},{"location":"formula-cheat-sheet/","text":"Formula Cheat Sheet # Grist has a powerful data engine to calculate the cells of your tables using formulas. Grist formulas are written in Python , the most popular language for data science. We also have a suite of Excel-like functions , with all-uppercase names. Here are some helpful notes: Formulas apply to the entire column Fields are included in formulas as $ColumnID . Python is case-sensitive, including for Grist table and column names. If your column ID is title , the formula will use $title , where both are lowercase. You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. If you don\u2019t see what you\u2019re looking for, post in the Community Forum and we\u2019ll be able to help you out! Math Functions # Simple Math (add, subtract, multiply divide) # Uses + , - , / and * operators to complete calculations. Example of Simple Math # Chestwood Art Studio ships art across the country and has the option of monthly payments over the course of 12 months. We have the subtotal, the tax (based on the state it is shipping to) and Amount Due Monthly. This formula column uses addition, multiplication and division. The formula used here is: ($Subtotal + ($Subtotal*$Tax)) / 12 We add the subtotal to the calculated tax then divide this by 12 months to get our Amount Due Monthly. Troubleshooting Errors # #TypeError : Confirm all columns used in the formula are of Numeric type. max and min # Allows you to find the max or min values in a list. Examples using MAX() and MIN() # MAX() and MIN() when capitalized are spreadsheet functions which require a specific syntax. Spreadsheet formula syntax is summarized in our functions reference . max() and min() in lowercase are Python functions. Max : Classes table of the Class Enrollment template. The formula used in the \u2018Spots Left\u2019 column of the Classes table is: max($Max_Students - $Count, 0) or \"Full\" This formula shows the number of spots remaining in a class, or the text \u2018Full\u2019 when the class is full or oversubscribed. We build a list between the parenthesis consisting of two items: $Max_Students - $Count and 0 . The formula returns whichever is greater. When $Count is less than $Max_Students , the difference $Max_Students - $Count is positive and represents the spots left in the class. When $Count exceeds $Max_Students , then the class is full or oversubscribed, and $Max_Students - $Count is negative. The maximum of a negative number and 0 will be 0, so max($Max_Students - $Count, 0) is 0. This represents a full class. The addition of or \"Full\" is applied when the value is falsy, which means that a 0 is replaced with the text \"Full\" . Min : Contacts table of the Lightweight CRM template. The formula used in the \u2018Due\u2019 column of the Contacts table is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Let\u2019s break this down. Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") finds all records in the Interactions table where the Contacts match and the Type is To-Do. This returns a list of records that we assign to the variable items . Next, we use dot notation to find all Dates assigned to the records in our items list. These dates are evaluated to find the minimum date. This is the value that is returned. So, we see the date of the task that is due the soonest. If there are no items in the list, nothing is returned and the field is left blank. In the MAX() example, the list has two items: $Max_Students - $Count and 0 , and the formula returns whichever is greater. In the min() example, the variable items is pulling a list of records based on the lookupRecords arguments, listing the dates, and returning the smallest date. Note that this is a Python function. If we had written the formula as MIN(), a spreadsheet function, the formula would not work because the spreadsheet formula requires a very specific format . Sum # Use the SUM() function when you want to sum a list of values available within a cell. If you want to sum values in a column, use Summary Tables . Example of SUM() # Custom Product Builder template The formula used in the Total Cost column of the Select or Add New Products table is: SUM($Requirements.Cost) The Requirements column is a hidden column in this table. It is a reference list column that pulls data from the Build Requirements table. Our formula uses the Requirements column to access the Build Requirements table then pulls the cost for each record in the table. We use SUM() to sum the costs from each record. Inventory Manager template The formula used in the Received column of the All Products table is: SUM(Incoming_Order_Line_Items.lookupRecords(SKU=$id).Received_Qty) We use the lookupRecords function to find all records in the Incoming Order Line Items table where the SKU matches the SKU in this row then pull the value in the Received Qty column for each of those records. We use SUM() to find the sum of those values. The Qty on Order and Sold columns of the All Products table are also great examples of the SUM() function. Check out another example in our Community Forum: Creating a Sum of Net and Gross profit from multiple tables Comparing for equality: == and != # When comparing for equality in Python, we use == for \u2018equals\u2019 and != for \u2018does not equal\u2019. If $A is 2 and $B is 3, the formula $A == $B would return False , while the formula $A != $B would be True . Examples using == # Inventory Manager template The formula used in the Received Qty column of the Incoming Order Line Items table is: if $Order.Status =='Received': return $Qty else: return None The Order column of the Incoming Order Line Items Table is a reference column that points to the Order Number column of the Incoming Orders table. $Order.Status uses dot notation to pull the value from the Status column of the Incoming Orders table. If the value in this column is equal to Received , the value from the Qty column will be returned. If the value is not equal to Received , nothing is returned. The formula used in the Date Received column of the Create New Order table is: if $Status == \"Received\": return NOW() This is a trigger formula that is triggered when a change is made to the Status column. If the value in the Status column is equal to Received , the current date is returned. If the values are not equal, nothing is returned. Examples using != # Project Management template The formula used in the Missed Deadline column of the Missed Deadline table is: TODAY()> $Due_Date and $Status != \"Completed\" If the current date is greater than the date given in the Due Date column and the value in the Status column is not equal to Completed , the formula is True . If either of these statements is false, the formula is False . Comparing Values: < , > , <= , >= # Allows you to compare numerical values. If Sales is equal to 1200 and Running_Cost is equal to 1650 , \"Gains\" if $Sales > $Running_Cost else \"Loss\" would return Loss . Examples comparing values # Inventory Manager template The formula used in the Stock Alert column of the All Products table is: if $In_Stock + $QTY_on_Order > 5: return \"In Stock\" if $In_Stock + $QTY_on_Order > 0: return \"Low Stock\" else: return \"OUT OF STOCK\" Here, we have two different if-return statements; if x is true, return some_value . Once a statement is true and a value is returned, the formula stops. If both are false, OUT OF STOCK is returned. First, if the value in the In Stock column plus the value in the Qty On Order column are greater than 5, return \u201cIn Stock\u201d. Next, if the value in the In Stock column plus the value in the Qty On Order column are greater than 0, return \u201cLow Stock\u201d. It\u2019s implied that the value is less than or equal to 5 because the first statement would have to be false for this to be evaluated. Last, if all statements are false, return \u201cOUT OF STOCK\u201d. Internal Links Tracker for SEO template The formula used in the Orphaned? column of the Orphaned Pages table is: len(Links.lookupRecords(To=$id))<1 We use the lookupRecords function to find all records in the Links table where the link in the To column matches the link listed in the Slug column of this row. We use len() to count the number of records found. If it\u2019s less than 1, the formula is evaluated to be true and the checkbox will be checked. If it\u2019s equal to or greater than 1, the formula is evaluated to be false. Converting from String to Float # String : A sequence of characters or snippets of text. In code, strings are quoted e.g. 'Hello' or \"-12\" (those are three characters in quotes, as opposed to a negative number). See Python str() Function for converting a specified value to a string. Float : Real numbers that can store decimal values. Also called floating point number. See Python float() Function for converting a specified value into a floating point number. Integer : A whole number, without decimals. See Python int() Function for converting a specified value to an integer number. Example converting a string to a float # Artwork Orders The formula used in the Sale Price column is: if $Appraisal_Value.endswith(\"k\"): return float($Appraisal_Value.rstrip(\"k\")) * 1000 return float($Appraisal_Value) In this example, the Appraisal Value column is a text column that contains alpha-numeric characters. In order to use this value in mathematical formulas, we need to convert from string to float. If the value in the Appraisal Value column ends with \u201ck\u201d, we first use rstrip() to strip \u201ck\u201d from the string in the Appraisal Value column. Now that we only have numeric characters, we use float() to convert our string to a float. Because K represents 1000 and we have removed this from the value, we multiply our float by 1000. If the value in the Appraisal Value column does not end with \u201ck\u201d, and only contains numeric characters, we can simply use float() to convert our string to a float. Troubleshooting # if you are trying to use different columns with numeric values in a mathematical formula but seeing an error, check the column types for each of the columns used in the formula. All need to be of type Numeric . float() is only needed when dealing with alpha-numeric values like we see in the example . TypeError: can\u2019t multiply sequence by non-int of type \u2018float\u2019 This error occurs when a formula attempts to multiply values from multiple columns, at least one of which is not a Numeric type column. In the screenshot below, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for /: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to divide values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the \u2018# of Payments\u2019 column is a Choice column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for +: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to add values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for -: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to subtract values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Discount column is a Text column. When we change the column type to Numeric , the error is resolved. Rounding Numbers # Specify the number of decimal places to give in a result using the ROUND() function. If Average Temperature is equal to 46.5 , ROUND($Average_Temperature) would return 47 Example of rounding numbers # Payroll template The formula used in the Payment column is: ROUND($Hours*$Per_Hour, 2) The ROUND() function follows the format ROUND(value, places) which will round the given value to the number of places specified. Our formula finds the value for $Hours*$Per_Hour then rounds this value to 2 decimal places. Mixing Products The formula used in the Rounded Value column is: mix_list_str = $Mix_Product.Lt_per_100_Lt mix_list_float = [float(i) for i in mix_list_str] x = [Lt * $Water/100 for Lt in mix_list_float] round_x = [ROUND(num, 2) for num in x] l = $Mix_Product.Product ' '.join('{} {}'.format(first, second) for first, second in zip(l, round_x)) Let\u2019s break this down. $Mix_Product represents the Mix Product column, a reference list column that pulls data from the Product column of Table1. We can use this column as a link to Table1 to pull other data. $Mix_Product.Lt_per_100_Lt uses the reference list column, Mix Product, to pull values from the Lt per 100 Lt column of Table1 for the products listed in the Mix Product column of Table2 then assigns this list of values to the variable mix_list_str . This is the same formula used in the Lt per 100 Lt column of Table2 so you can see the value it returns in row 1 of Table2. It returns a list: ['0.2355', '1.2579'] . This list is evaluated as a string rather than numerical values. We need to convert each value in this list to a float. In our next formula, [float(i) for i in mix_list_str] , we iterate through the list that was assigned in the first equation to mix_list_str and convert each value to a floating-point number. We want to convert to a float rather than integer because not all values are whole numbers and contain decimals. i is a variable representing each value. So each value in mix_list_str is evaluated in the equation float(i) . float(0.2355) converts 0.2355 to a float and float(1.2579) converts 1.2579 to a float. Now, we assign our list of floats to the variable mix_list_float . We can now use our float values in a mathematical equation. Once again, we iterate through the list that was assigned to the variable mix_list_float . In our equation [Lt * $Water/100 for Lt in mix_list_float] , Lt represents each value in mix_list_float and $Water represents the value found in the Water column which is 1000 . We evaluate the equation Lt * 1000/100 when Lt = 0.2355 and Lt = 1.2579 which returns the list [2.355, 12.579] . We assign this list to the variable x . To round the values in x to two decimal places, we need to evaluate the equation ROUND(num, 2) where num represents each value in our list and 2 specifies the number of decimal places we want to round to. This returns the list [2.36, 12.58] which we assign to the variable round_x. In the first equation, we used our reference list column, Mix Product, as our link to Table1 in order to pull data from Table1 into Table2. We use this method again in $Mix_Product.Product to pull data from the Product column of Table1. This returns a list of products; [Prod A, Prod B] . We assign this list to the variable l . Finally, we use the join() method to combine our two lists. ' ' is our starting (empty) string. We use Python\u2019s format method to format our string. {} is a placeholder for each variable listed in .format() . Last, we use Python\u2019s zip() function to pair the first values from each list together and then pair the second values in each list together. l is assigned as our first list and round_x is assigned as our second list. l = [Prod A, Prod B] and round_x = [2.36, 12.58] . Zipping our lists into '{} {}'.format(first, second) gives us Prod A 2.36 in our first iteration and Prod B 12.58 in our second iteration. Our final return value is Prod A 2.36 Prod B 12.58 . Formatting numbers with leading zeros # Allows you to specify the minimum number of digits returned in a numerical column by adding leading zeros. If x = 5, str(x).zfill(3) or '{:0>3}'.format(x) would return 005 . Formatting numbers with leading zeros # Community Example: Using Row ID The formula used in the 5-digit ID column of the ID Examples table is: 'TCH{:0>5}'.format($id) '{:0>5}'.format($id) takes the unique row ID and formats it to be a minimum of 5 digits. We then add this to our string \"TCH\" to get our final value. If the $id is longer than 5 digits, the formula returns the string unmodified. We can do the same thing using the str.zfill() method. The formula used in the zfill Method column of the ID Examples table is: str($id).zfill(5) str($id) converts the row ID to a string. .zfill(x) returns a copy of the string with leading zeros to make a string of length x . In our example, it adds leading zeros to make the string 5 characters in length. Again, if the $id is longer than 5 digits, the formula returns the string unmodified. Troubleshooting Errors # #TypeError : can only concatenate str (not \u201cint\u201d) to str If you mean to combine a string and a numerical value, be sure to convert it to string using str() . Working with Strings # Combining Text From Multiple Columns # Method 1: If you have a First Name column and a Last Name column, you can combine these columns to have a Full Name column. If First Name is George and Last Name is Washington , $First_Name + \" \" + $Last_Name would return George Washington . Method 2: If you have additional formatting, an easier way to do this would be using Python\u2019s String format() method . The format() method formats the specified value(s) and inserts them in place of the placeholder, {} . Using the same example as above, our formula would be \"{} {}\".format($First_Name, $Last_Name) . Note: You can click on columns to insert them into your formulas, rather than typing them in. Examples using Method 1 # Class Enrollment template{:target=\u201d_blank\u201d} The formula used in the Full Name column of the Students table is: $Last_Name + \", \" + $First_Name Here, we are combining the value found in the Last Name column with a comma followed by a space followed by the value from the First Name column. When adding any extra characters or spaces, place these between double quotes, as we did in the example with \", \" . An alternative combination of these columns for Full Name could be $First_Name + \" \" + $Last_Name . For the example in row 1, First Name is Brockie and Last Name is Raddon so the value returned would be Brockie Raddon . Inventory Manager template The formula used in the SKU column of the All Products table is: $Brand.Brand_Code+\"-\"+$Color.Code+\"-\"+$Size Brand is a reference column that pulls data from the Name Brand column of the Add Products table. We use this reference column in $Brand.Brand_Code to pull data from the Brand Code column of the Add Products table. Color is a reference column that pulls data from the Color column of the Color table. We use this reference column in $Color.Code to pull data from the Code column of the Color table. Each of the values found in $Brand.Brand_Code and $Color.Code are combined with the value in the Size column with a - between each of the three values to make up the SKU. Examples using Method 2 # Tracking Time + Invoicing template The formula used in the Project Name column of the Projects table is: \"{}: {}\".format($Client.Name, $Name) Let\u2019s break this down. Everything between double quotes \" is our string. The curly brackets {} are placeholders for the values found using .format() which is Python\u2019s string format() method. The first set of curly brackets are replaced with the value found in $Client.Name . Client is a reference column that pulls data for a specific record from the Clients table. $Client.Name is using our reference column, Client to pull data from the Name column of the Clients table. The second set of curly brackets are replaced with the value found in the Name column of this table. Although the Client column shows the value that we want, we can\u2019t use $Client like we did $Name . This is because the Client column is a reference column. It is referencing the entire record but uses the value from the Name column of the Clients table as a visual representation of that record. Under the column configuration panel on the right hand side, we can change what column value we see for the record. In the screenshot below, \u2018Show Column\u2019 was changed from Name to Email. It doesn\u2019t change the data, it just changes the label on that data in the Client column. It\u2019s still pointing to the same record but now shows a different label. $Client.Name pulls the Name for the record that is referenced in the Client column, regardless of the label we see. Custom Product Builder template The formula used in the All Components column of the CONTRACT_BUILDER Card is: '\\n'.join(sorted( \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() )) We are using the join() method , sorted() function and format() method method all in one! '\\n'.join() adds a new line between each item in the list. sorted() sorts the items in the list alphabetically. This leaves us with the following: \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() We\u2019ll work through this backwards. First, we need to take a look at the Components column which is a hidden column in the All Contracts table. This column is a list of components and their associated quantities for the contract. In the for loop, we assign each item in the list of components two variables, comp and quantity. For Components[3]: 6.0 , comp = Components[3] and quantity = 6.0 . Components[#] specifies a Component in the Components table by Row ID. Components[3] is the component assigned 3 as it\u2019s row id. Now, we run each item from the list above through the equation \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) . comp.Component replaces the first set of {} . comp is the variable with our component ID so comp.Component finds the value in the Component column associated with that row ID. For Components[3] , comp.Component is Nozzle. quantity replaces the second set of {} . Again, the quantity is the second variable in our list. For Components[3]: 6.0 , quantity is 6.0 . Our second set of {} are not empty. They include :g *. This converts the value to a floating-point number. comp.Unit replaces the last set of {} . comp is the variable with our component ID so comp.Unit finds the value in the Unit column associated with that row ID. For Components[3] , comp.Unit is None . *Note that {:g} formats floating point numbers in a particular way that omit decimals when they aren\u2019t needed. There are many options available within placeholders for formatting numbers, dates, etc. Learn more about placeholders here . Email Contacts template The formula used in the Body column of the Advanced Compose table is: \"Dear %s,\\n\\nWelcome to the %s team!\" % ($Contact_Name_as_Plaintext, $Team) This technique uses the % operator instead of the format() method. Format specifiers begin with % followed by a character that represents the data type. %s is a placeholder for a string. The first %s is replaced with the value found in the \u201cContact Name as Plaintext\u201d column which is a hidden column and the second %s is replaced by the value in the Team column. \\n adds a new line. Splitting Strings of Text # Split a string using Python\u2019s split() method. If Full Name is George Washington , $Full_Name.split(\" \") would return [George, Washington] . Example of Splitting Strings of Text # Community Example: Colors The formula in the \u201cColor Reference (Just URL)\u201d column of Table 2 is: split = $Color_Reference.Color.split(\" \") return split[-1] $Color_Reference.Color uses the reference column, \u201cColor Reference\u201d to pull data from the table it is referencing, Table 1. Specifically, it pulls the value from the Color column of Table 1. Color is a text column that contains a hyperlink with a label. We only see the label in Table 1 but as you can see in the screenshot above, the value in the \u2018pink\u2019 cell is expanded to show the entire string which contains \u201cpink\u201d followed by the URL. You can also see this in the \u201cColor Reference\u201d column of Table 2. We want to get the link by itself in \u201cColor Reference (Just URL)\u201d. We can do this using Python\u2019s split() method. .split(\" \") allows us to split the string anywhere there is a space (\" \") . In the Color column, there is a label followed by a space followed by the URL. The value from the Color column is split into a list containing two items Label and URL . This list is assigned to the variable split . We want to return the last item in the list split in order to get our URL . The last item in a list always has index [-1] . return split[-1] returns the last item in the list split . Direct Link to Gmail History for a Contact # If you store contacts in Grist, and use Gmail to email them, you can create a formula that will open Gmail to a list of conversations with that contact. Read about it in the Community: Pull up Gmail history for a particular contact Troubleshooting # Is your URL still showing after you added a label? Make sure your Column Type is Text and Cell Format is Hyperlink. Joining a List of Strings # When you want to join a list of strings, you can use Python\u2019s join() method . Example of Joining a List # Community Example: .join() Example The formula used in the Advertisement column of the 2022 Grand Openings table is: \"Coming soon to a city near you!\\n\" + \" : \".join($New_Location_s_in_2022) Here, we are joining multiple strings to create our advertisement. \"Coming soon to a city near you!\\n\" is returned almost exactly as we see it, minus the quotes \"\" and \\n at the end of the string. The quotes \"\" specify that this is a string and \\n is actually a newline character that can be used to specify a new line within a string. \" : \".join($New_Location_s_in_2022) is also a string but uses Python\u2019s join() method to join values from our choice list column, \u201cNew Locations in 2022\u201d. What we see in quotes before .join is what will separate each value in our list. In this example, each value is separated by a space, : and another space. Finding Duplicates # You can find duplicates in a column using either conditional formatting or a helper column. Read about it in the Community: Ensure unique values or detect duplicates Example of Finding Duplicates # Community Example: Finding Duplicates The formula used in the Duplicate? column of the Duplicates table is: len(Duplicates.lookupRecords(Grocery_List=$Grocery_List))>1 Let\u2019s break this down, working from the inside > out. Duplicates.lookupRecords(Grocery_List=$Grocery_List) This is a lookupRecords function that follows the format of: [Table_Name].lookupRecords([A]=$[B]) Where [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. This formula looks up records in the Duplicates table where a record in the Grocery List column matches another record in the same column. len() counts the number of records in our list. Since each duplicate will match with the other, it should appear twice in our list. This is why len() > 1 . if len() > 1 , the formula is true. If len() <= 1 , the formula is false. This same formula can be used in conditional formatting. This can be seen in the \u2018Grocery List\u2019 column of the Duplicates table. If len() > 1 , our formula is true and the conditional cell color is applied to these cells. If len() <= 1 , our formula is false and the cell color is unchanged. Using a Record\u2019s Unique Identifier in Formulas # When a record is created, it is assigned a numeric id (available as $id in formulas) that is unique within that table. You can reveal the row ID by adding a formula column where the formula is $id . Examples Using Row ID in Formulas # You can reveal the ID with the formula $id Custom Product Builder template The formula used in the Contract No. column of the Contract Builder table is: $id + 500 Here, we are using a trigger formula to create a unique Contract Number when a record is created. Class Enrollment template The formula used in the Count column of the Classes table is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This is refered to as a Reverse Lookup. We can use the row id to match a record in another table where a reference column is used. LookupRecords follows the format [Table_Name].lookupRecords([A]=$[B]) . [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Lookup Records creates a list of records that match the criteria listed. len() counts how many records are in that list. Here, we are looking up records from the Enrollments table where the record called out in the Class column (our reference column) has the same row ID as the row in the table you are entering the formula. Additionally, the value in the Status column of the Enrollments table is Confirmed . We\u2019ll walk through this. The table we are looking up records in is the Enrollments table. Our criteria comes from the Class column and the Status column.The criteria for Status is straightforward; the value must be Confirmed in order to be included in our list of records. Class is a bit more complicated. As we see in the screenshot below, Class is a reference column that pulls data from the Classes table. Here, the Class column shows 2018F-Stars . A reference column points to the entire record, not just the value you see here in the Class column. Using the configuration panel on the right hand side of the screen, you can pick any column from the originating table to show. For this example, the Class column shows the value from the Class Code column of the Classes table but it points to the entire record where the class code is 2018F-Stars . As you can see in this screenshot, the Row ID for this particular record is 2 and because we are calculating the Count for the row with Row ID = 2 , it will count all records in the Enrollment table where Class shows 2018F-Stars and Status is Confirmed . Restaurant Custom Orders template The trigger formula used in the BOM # column of the Bill of Materials table is: MAX(o.BOM_ for o in Bill_Of_Materials.all if o.id != $id) + 1 First, we\u2019ll walk through the formula inside the parenthesis then work outwards. Here, o is a variable representing each record in our table. o.BOM_ represents the BOM # for each record and o.id represents the row ID for each record. This is a for loop that makes a list of the BOM # for each record in the table Bill of Materials when the record ID does not equal the ID of this row. MAX() finds the maximum BOM # in the list then + 1 to get our final value. This is a trigger formula that only applies to new records. When a new record is created, the formula finds the highest BOM # in the table then adds 1 so we have a unique BOM # for the new record. Removing Duplicates From a List # You can remove duplicates from a list with help from Python\u2019s set() method. Example of Removing Duplicates from a List # Community Example: Removing Duplicates From a List The formula in the All Divisions column of the Abroad Trips table is: confirmed_div = $Attending_Confirmed.Role_Division.Division pending_div = $Attending_Pending.Role_Division.Division full_list = confirmed_div + pending_div return sorted(set(full_list)) We will walk through this one line at a time. Attending-Confirmed is a Reference List column that pulls data from the EMPLOYEES table. $Attending_Confirmed.Role_Division pulls the value from the Role Division column of the EMPLOYEES table. The Role Division column in the EMPLOYEES table is a reference column itself, which points to a record in the Divisions table. Chaining allows us to specify what information we want from this record. In this case, we want the Division. We expand our formula to $Attending_Confirmed.Role_Division.Division . The Division is found for each employee listed in the Attending-Confirmed column, creating a list. We assign this list of divisions to the variable confirmed_div . Attending-Pending is also a Reference List column that pulls data from the EMPLOYEES table. $Attending_Pending.Role_Division.Division does the same as above except now we pull the division for each employee in the Attending-Pending column. We assign this list to the variable pending_div . We create a list of all divisions by adding the two lists together and assigning this combined list to the variable full_list . return sorted(set(full_list)) first uses Python\u2019s set() method to create a list with no duplicate divisions. We then use the sorted() method to return our set of divisions, sorted alphabetically. Note that the formula above can be simplified even further to: sorted( set($Attending_Confirmed.Role_Division.Division) | set($Attending_Pending.Role_Division.Division) ) Setting Default Values for New Records # You can set default values for when a new record is created and save yourself the trouble of having to fill in the same fields with the same values time after time. Read about it in the Community: Default values on the widget Working with dates and times # Automatic Date, Time and Author Stamps # You can automatically add the date or time a record was created or updated as well as who made the change. Examples of Automatic Date, Time and Author Stamps # Grant Application Tracker template The formula used in the Last Updated column of the Tasks table is: NOW() This is a trigger formula that triggers when a change is made to any field for this record. When a change is made, this formula runs its calculation. NOW() calculates the current time and date for the time zone selected. The formula used in the Created By column of the Tasks table is: user.Name This is a trigger formula that triggers when a new record is created. When the record is created, this formula runs its calculation. user.Name looks up the user account that is logged into Grist and returns the name associated with that account. Troubleshooting Errors # If the time value in your datetime column is not calculating, check your formula. If TODAY() is used in DateTime, the time will always show 12:00am as you see below. NOW() is used for DateTime columns. TODAY() is used for Date. #AttributeError You have likely entered user.name but the formula is user.Name . Keep an eye on capitalization! #NameError You may have entered username or userName . The correct formula is user.Name . Another possibility is that this was entered in as a Formula column rather than a trigger formula column. Convert it to a trigger formula and this should resolve the problem. Filtering Data within a Specified Amount of Time # Using the DATEADD() function and comparision operators , you can determine if a date falls within a specific range then apply a filter. Example Filtering Data that \u2018Falls in 1 Month Range` # Community Example: Filtering Data Within a 1-Month Range The formula used in the Falls in 1 Month Range? column of the Interactions table is: TODAY() >= $Date >= DATEADD(TODAY(),months=-1) TODAY() returns the current date. $Date is the name of a column in our table, which is a Date type column. DATEADD(start_date, days=0, months=0, years=0, weeks=0) returns the date that is the given number of days, months, years, or weeks before or after the start_date . In this example, it returns the date that is one month prior to the start date, TODAY() . This formula is true if the date value in the Date column falls between TODAY() and our DATEADD() date which is one month ago. If the date value in the Date column does not fall between these two dates, the formula returns false. We can use this column to filter our data. If we only want to see interactions that fall within the 1 Month Range, we would filter to only include true values. If we want to see interactions that fall outside of the 1 Month Range, we would filter to only include false values. Troubleshooting Errors # #TypeError : Because $Date is a Date type column, TODAY() must be used in formulas comparing dates. NOW() is a DateTime formula that should only be used with other DateTime values. For example, if the $Date column was a DateTime type column, NOW() would need to be used rather than TODAY() because it includes the time component. NOW() is date and time. TODAY() is only date.","title":"Formula Cheat Sheet"},{"location":"formula-cheat-sheet/#formula-cheat-sheet","text":"Grist has a powerful data engine to calculate the cells of your tables using formulas. Grist formulas are written in Python , the most popular language for data science. We also have a suite of Excel-like functions , with all-uppercase names. Here are some helpful notes: Formulas apply to the entire column Fields are included in formulas as $ColumnID . Python is case-sensitive, including for Grist table and column names. If your column ID is title , the formula will use $title , where both are lowercase. You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. If you don\u2019t see what you\u2019re looking for, post in the Community Forum and we\u2019ll be able to help you out!","title":"Formula Cheat Sheet"},{"location":"formula-cheat-sheet/#math-functions","text":"","title":"Math Functions"},{"location":"formula-cheat-sheet/#simple-math-add-subtract-multiply-divide","text":"Uses + , - , / and * operators to complete calculations.","title":"Simple Math (add, subtract, multiply divide)"},{"location":"formula-cheat-sheet/#example-of-simple-math","text":"Chestwood Art Studio ships art across the country and has the option of monthly payments over the course of 12 months. We have the subtotal, the tax (based on the state it is shipping to) and Amount Due Monthly. This formula column uses addition, multiplication and division. The formula used here is: ($Subtotal + ($Subtotal*$Tax)) / 12 We add the subtotal to the calculated tax then divide this by 12 months to get our Amount Due Monthly.","title":"Example of Simple Math"},{"location":"formula-cheat-sheet/#troubleshooting-errors","text":"#TypeError : Confirm all columns used in the formula are of Numeric type.","title":"Troubleshooting Errors"},{"location":"formula-cheat-sheet/#max-and-min","text":"Allows you to find the max or min values in a list.","title":"max and min"},{"location":"formula-cheat-sheet/#examples-using-max-and-min","text":"MAX() and MIN() when capitalized are spreadsheet functions which require a specific syntax. Spreadsheet formula syntax is summarized in our functions reference . max() and min() in lowercase are Python functions. Max : Classes table of the Class Enrollment template. The formula used in the \u2018Spots Left\u2019 column of the Classes table is: max($Max_Students - $Count, 0) or \"Full\" This formula shows the number of spots remaining in a class, or the text \u2018Full\u2019 when the class is full or oversubscribed. We build a list between the parenthesis consisting of two items: $Max_Students - $Count and 0 . The formula returns whichever is greater. When $Count is less than $Max_Students , the difference $Max_Students - $Count is positive and represents the spots left in the class. When $Count exceeds $Max_Students , then the class is full or oversubscribed, and $Max_Students - $Count is negative. The maximum of a negative number and 0 will be 0, so max($Max_Students - $Count, 0) is 0. This represents a full class. The addition of or \"Full\" is applied when the value is falsy, which means that a 0 is replaced with the text \"Full\" . Min : Contacts table of the Lightweight CRM template. The formula used in the \u2018Due\u2019 column of the Contacts table is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Let\u2019s break this down. Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") finds all records in the Interactions table where the Contacts match and the Type is To-Do. This returns a list of records that we assign to the variable items . Next, we use dot notation to find all Dates assigned to the records in our items list. These dates are evaluated to find the minimum date. This is the value that is returned. So, we see the date of the task that is due the soonest. If there are no items in the list, nothing is returned and the field is left blank. In the MAX() example, the list has two items: $Max_Students - $Count and 0 , and the formula returns whichever is greater. In the min() example, the variable items is pulling a list of records based on the lookupRecords arguments, listing the dates, and returning the smallest date. Note that this is a Python function. If we had written the formula as MIN(), a spreadsheet function, the formula would not work because the spreadsheet formula requires a very specific format .","title":"Examples using MAX() and MIN()"},{"location":"formula-cheat-sheet/#sum","text":"Use the SUM() function when you want to sum a list of values available within a cell. If you want to sum values in a column, use Summary Tables .","title":"Sum"},{"location":"formula-cheat-sheet/#example-of-sum","text":"Custom Product Builder template The formula used in the Total Cost column of the Select or Add New Products table is: SUM($Requirements.Cost) The Requirements column is a hidden column in this table. It is a reference list column that pulls data from the Build Requirements table. Our formula uses the Requirements column to access the Build Requirements table then pulls the cost for each record in the table. We use SUM() to sum the costs from each record. Inventory Manager template The formula used in the Received column of the All Products table is: SUM(Incoming_Order_Line_Items.lookupRecords(SKU=$id).Received_Qty) We use the lookupRecords function to find all records in the Incoming Order Line Items table where the SKU matches the SKU in this row then pull the value in the Received Qty column for each of those records. We use SUM() to find the sum of those values. The Qty on Order and Sold columns of the All Products table are also great examples of the SUM() function. Check out another example in our Community Forum: Creating a Sum of Net and Gross profit from multiple tables","title":"Example of SUM()"},{"location":"formula-cheat-sheet/#comparing-for-equality-and","text":"When comparing for equality in Python, we use == for \u2018equals\u2019 and != for \u2018does not equal\u2019. If $A is 2 and $B is 3, the formula $A == $B would return False , while the formula $A != $B would be True .","title":"Comparing for equality: == and !="},{"location":"formula-cheat-sheet/#examples-using","text":"Inventory Manager template The formula used in the Received Qty column of the Incoming Order Line Items table is: if $Order.Status =='Received': return $Qty else: return None The Order column of the Incoming Order Line Items Table is a reference column that points to the Order Number column of the Incoming Orders table. $Order.Status uses dot notation to pull the value from the Status column of the Incoming Orders table. If the value in this column is equal to Received , the value from the Qty column will be returned. If the value is not equal to Received , nothing is returned. The formula used in the Date Received column of the Create New Order table is: if $Status == \"Received\": return NOW() This is a trigger formula that is triggered when a change is made to the Status column. If the value in the Status column is equal to Received , the current date is returned. If the values are not equal, nothing is returned.","title":"Examples using =="},{"location":"formula-cheat-sheet/#examples-using_1","text":"Project Management template The formula used in the Missed Deadline column of the Missed Deadline table is: TODAY()> $Due_Date and $Status != \"Completed\" If the current date is greater than the date given in the Due Date column and the value in the Status column is not equal to Completed , the formula is True . If either of these statements is false, the formula is False .","title":"Examples using !="},{"location":"formula-cheat-sheet/#comparing-values","text":"Allows you to compare numerical values. If Sales is equal to 1200 and Running_Cost is equal to 1650 , \"Gains\" if $Sales > $Running_Cost else \"Loss\" would return Loss .","title":"Comparing Values: < , > , <= , >="},{"location":"formula-cheat-sheet/#examples-comparing-values","text":"Inventory Manager template The formula used in the Stock Alert column of the All Products table is: if $In_Stock + $QTY_on_Order > 5: return \"In Stock\" if $In_Stock + $QTY_on_Order > 0: return \"Low Stock\" else: return \"OUT OF STOCK\" Here, we have two different if-return statements; if x is true, return some_value . Once a statement is true and a value is returned, the formula stops. If both are false, OUT OF STOCK is returned. First, if the value in the In Stock column plus the value in the Qty On Order column are greater than 5, return \u201cIn Stock\u201d. Next, if the value in the In Stock column plus the value in the Qty On Order column are greater than 0, return \u201cLow Stock\u201d. It\u2019s implied that the value is less than or equal to 5 because the first statement would have to be false for this to be evaluated. Last, if all statements are false, return \u201cOUT OF STOCK\u201d. Internal Links Tracker for SEO template The formula used in the Orphaned? column of the Orphaned Pages table is: len(Links.lookupRecords(To=$id))<1 We use the lookupRecords function to find all records in the Links table where the link in the To column matches the link listed in the Slug column of this row. We use len() to count the number of records found. If it\u2019s less than 1, the formula is evaluated to be true and the checkbox will be checked. If it\u2019s equal to or greater than 1, the formula is evaluated to be false.","title":"Examples comparing values"},{"location":"formula-cheat-sheet/#converting-from-string-to-float","text":"String : A sequence of characters or snippets of text. In code, strings are quoted e.g. 'Hello' or \"-12\" (those are three characters in quotes, as opposed to a negative number). See Python str() Function for converting a specified value to a string. Float : Real numbers that can store decimal values. Also called floating point number. See Python float() Function for converting a specified value into a floating point number. Integer : A whole number, without decimals. See Python int() Function for converting a specified value to an integer number.","title":"Converting from String to Float"},{"location":"formula-cheat-sheet/#example-converting-a-string-to-a-float","text":"Artwork Orders The formula used in the Sale Price column is: if $Appraisal_Value.endswith(\"k\"): return float($Appraisal_Value.rstrip(\"k\")) * 1000 return float($Appraisal_Value) In this example, the Appraisal Value column is a text column that contains alpha-numeric characters. In order to use this value in mathematical formulas, we need to convert from string to float. If the value in the Appraisal Value column ends with \u201ck\u201d, we first use rstrip() to strip \u201ck\u201d from the string in the Appraisal Value column. Now that we only have numeric characters, we use float() to convert our string to a float. Because K represents 1000 and we have removed this from the value, we multiply our float by 1000. If the value in the Appraisal Value column does not end with \u201ck\u201d, and only contains numeric characters, we can simply use float() to convert our string to a float.","title":"Example converting a string to a float"},{"location":"formula-cheat-sheet/#troubleshooting","text":"if you are trying to use different columns with numeric values in a mathematical formula but seeing an error, check the column types for each of the columns used in the formula. All need to be of type Numeric . float() is only needed when dealing with alpha-numeric values like we see in the example . TypeError: can\u2019t multiply sequence by non-int of type \u2018float\u2019 This error occurs when a formula attempts to multiply values from multiple columns, at least one of which is not a Numeric type column. In the screenshot below, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for /: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to divide values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the \u2018# of Payments\u2019 column is a Choice column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for +: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to add values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for -: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to subtract values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Discount column is a Text column. When we change the column type to Numeric , the error is resolved.","title":"Troubleshooting"},{"location":"formula-cheat-sheet/#rounding-numbers","text":"Specify the number of decimal places to give in a result using the ROUND() function. If Average Temperature is equal to 46.5 , ROUND($Average_Temperature) would return 47","title":"Rounding Numbers"},{"location":"formula-cheat-sheet/#example-of-rounding-numbers","text":"Payroll template The formula used in the Payment column is: ROUND($Hours*$Per_Hour, 2) The ROUND() function follows the format ROUND(value, places) which will round the given value to the number of places specified. Our formula finds the value for $Hours*$Per_Hour then rounds this value to 2 decimal places. Mixing Products The formula used in the Rounded Value column is: mix_list_str = $Mix_Product.Lt_per_100_Lt mix_list_float = [float(i) for i in mix_list_str] x = [Lt * $Water/100 for Lt in mix_list_float] round_x = [ROUND(num, 2) for num in x] l = $Mix_Product.Product ' '.join('{} {}'.format(first, second) for first, second in zip(l, round_x)) Let\u2019s break this down. $Mix_Product represents the Mix Product column, a reference list column that pulls data from the Product column of Table1. We can use this column as a link to Table1 to pull other data. $Mix_Product.Lt_per_100_Lt uses the reference list column, Mix Product, to pull values from the Lt per 100 Lt column of Table1 for the products listed in the Mix Product column of Table2 then assigns this list of values to the variable mix_list_str . This is the same formula used in the Lt per 100 Lt column of Table2 so you can see the value it returns in row 1 of Table2. It returns a list: ['0.2355', '1.2579'] . This list is evaluated as a string rather than numerical values. We need to convert each value in this list to a float. In our next formula, [float(i) for i in mix_list_str] , we iterate through the list that was assigned in the first equation to mix_list_str and convert each value to a floating-point number. We want to convert to a float rather than integer because not all values are whole numbers and contain decimals. i is a variable representing each value. So each value in mix_list_str is evaluated in the equation float(i) . float(0.2355) converts 0.2355 to a float and float(1.2579) converts 1.2579 to a float. Now, we assign our list of floats to the variable mix_list_float . We can now use our float values in a mathematical equation. Once again, we iterate through the list that was assigned to the variable mix_list_float . In our equation [Lt * $Water/100 for Lt in mix_list_float] , Lt represents each value in mix_list_float and $Water represents the value found in the Water column which is 1000 . We evaluate the equation Lt * 1000/100 when Lt = 0.2355 and Lt = 1.2579 which returns the list [2.355, 12.579] . We assign this list to the variable x . To round the values in x to two decimal places, we need to evaluate the equation ROUND(num, 2) where num represents each value in our list and 2 specifies the number of decimal places we want to round to. This returns the list [2.36, 12.58] which we assign to the variable round_x. In the first equation, we used our reference list column, Mix Product, as our link to Table1 in order to pull data from Table1 into Table2. We use this method again in $Mix_Product.Product to pull data from the Product column of Table1. This returns a list of products; [Prod A, Prod B] . We assign this list to the variable l . Finally, we use the join() method to combine our two lists. ' ' is our starting (empty) string. We use Python\u2019s format method to format our string. {} is a placeholder for each variable listed in .format() . Last, we use Python\u2019s zip() function to pair the first values from each list together and then pair the second values in each list together. l is assigned as our first list and round_x is assigned as our second list. l = [Prod A, Prod B] and round_x = [2.36, 12.58] . Zipping our lists into '{} {}'.format(first, second) gives us Prod A 2.36 in our first iteration and Prod B 12.58 in our second iteration. Our final return value is Prod A 2.36 Prod B 12.58 .","title":"Example of rounding numbers"},{"location":"formula-cheat-sheet/#formatting-numbers-with-leading-zeros","text":"Allows you to specify the minimum number of digits returned in a numerical column by adding leading zeros. If x = 5, str(x).zfill(3) or '{:0>3}'.format(x) would return 005 .","title":"Formatting numbers with leading zeros"},{"location":"formula-cheat-sheet/#formatting-numbers-with-leading-zeros_1","text":"Community Example: Using Row ID The formula used in the 5-digit ID column of the ID Examples table is: 'TCH{:0>5}'.format($id) '{:0>5}'.format($id) takes the unique row ID and formats it to be a minimum of 5 digits. We then add this to our string \"TCH\" to get our final value. If the $id is longer than 5 digits, the formula returns the string unmodified. We can do the same thing using the str.zfill() method. The formula used in the zfill Method column of the ID Examples table is: str($id).zfill(5) str($id) converts the row ID to a string. .zfill(x) returns a copy of the string with leading zeros to make a string of length x . In our example, it adds leading zeros to make the string 5 characters in length. Again, if the $id is longer than 5 digits, the formula returns the string unmodified.","title":"Formatting numbers with leading zeros"},{"location":"formula-cheat-sheet/#troubleshooting-errors_1","text":"#TypeError : can only concatenate str (not \u201cint\u201d) to str If you mean to combine a string and a numerical value, be sure to convert it to string using str() .","title":"Troubleshooting Errors"},{"location":"formula-cheat-sheet/#working-with-strings","text":"","title":"Working with Strings"},{"location":"formula-cheat-sheet/#combining-text-from-multiple-columns","text":"Method 1: If you have a First Name column and a Last Name column, you can combine these columns to have a Full Name column. If First Name is George and Last Name is Washington , $First_Name + \" \" + $Last_Name would return George Washington . Method 2: If you have additional formatting, an easier way to do this would be using Python\u2019s String format() method . The format() method formats the specified value(s) and inserts them in place of the placeholder, {} . Using the same example as above, our formula would be \"{} {}\".format($First_Name, $Last_Name) . Note: You can click on columns to insert them into your formulas, rather than typing them in.","title":"Combining Text From Multiple Columns"},{"location":"formula-cheat-sheet/#examples-using-method-1","text":"Class Enrollment template{:target=\u201d_blank\u201d} The formula used in the Full Name column of the Students table is: $Last_Name + \", \" + $First_Name Here, we are combining the value found in the Last Name column with a comma followed by a space followed by the value from the First Name column. When adding any extra characters or spaces, place these between double quotes, as we did in the example with \", \" . An alternative combination of these columns for Full Name could be $First_Name + \" \" + $Last_Name . For the example in row 1, First Name is Brockie and Last Name is Raddon so the value returned would be Brockie Raddon . Inventory Manager template The formula used in the SKU column of the All Products table is: $Brand.Brand_Code+\"-\"+$Color.Code+\"-\"+$Size Brand is a reference column that pulls data from the Name Brand column of the Add Products table. We use this reference column in $Brand.Brand_Code to pull data from the Brand Code column of the Add Products table. Color is a reference column that pulls data from the Color column of the Color table. We use this reference column in $Color.Code to pull data from the Code column of the Color table. Each of the values found in $Brand.Brand_Code and $Color.Code are combined with the value in the Size column with a - between each of the three values to make up the SKU.","title":"Examples using Method 1"},{"location":"formula-cheat-sheet/#examples-using-method-2","text":"Tracking Time + Invoicing template The formula used in the Project Name column of the Projects table is: \"{}: {}\".format($Client.Name, $Name) Let\u2019s break this down. Everything between double quotes \" is our string. The curly brackets {} are placeholders for the values found using .format() which is Python\u2019s string format() method. The first set of curly brackets are replaced with the value found in $Client.Name . Client is a reference column that pulls data for a specific record from the Clients table. $Client.Name is using our reference column, Client to pull data from the Name column of the Clients table. The second set of curly brackets are replaced with the value found in the Name column of this table. Although the Client column shows the value that we want, we can\u2019t use $Client like we did $Name . This is because the Client column is a reference column. It is referencing the entire record but uses the value from the Name column of the Clients table as a visual representation of that record. Under the column configuration panel on the right hand side, we can change what column value we see for the record. In the screenshot below, \u2018Show Column\u2019 was changed from Name to Email. It doesn\u2019t change the data, it just changes the label on that data in the Client column. It\u2019s still pointing to the same record but now shows a different label. $Client.Name pulls the Name for the record that is referenced in the Client column, regardless of the label we see. Custom Product Builder template The formula used in the All Components column of the CONTRACT_BUILDER Card is: '\\n'.join(sorted( \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() )) We are using the join() method , sorted() function and format() method method all in one! '\\n'.join() adds a new line between each item in the list. sorted() sorts the items in the list alphabetically. This leaves us with the following: \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() We\u2019ll work through this backwards. First, we need to take a look at the Components column which is a hidden column in the All Contracts table. This column is a list of components and their associated quantities for the contract. In the for loop, we assign each item in the list of components two variables, comp and quantity. For Components[3]: 6.0 , comp = Components[3] and quantity = 6.0 . Components[#] specifies a Component in the Components table by Row ID. Components[3] is the component assigned 3 as it\u2019s row id. Now, we run each item from the list above through the equation \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) . comp.Component replaces the first set of {} . comp is the variable with our component ID so comp.Component finds the value in the Component column associated with that row ID. For Components[3] , comp.Component is Nozzle. quantity replaces the second set of {} . Again, the quantity is the second variable in our list. For Components[3]: 6.0 , quantity is 6.0 . Our second set of {} are not empty. They include :g *. This converts the value to a floating-point number. comp.Unit replaces the last set of {} . comp is the variable with our component ID so comp.Unit finds the value in the Unit column associated with that row ID. For Components[3] , comp.Unit is None . *Note that {:g} formats floating point numbers in a particular way that omit decimals when they aren\u2019t needed. There are many options available within placeholders for formatting numbers, dates, etc. Learn more about placeholders here . Email Contacts template The formula used in the Body column of the Advanced Compose table is: \"Dear %s,\\n\\nWelcome to the %s team!\" % ($Contact_Name_as_Plaintext, $Team) This technique uses the % operator instead of the format() method. Format specifiers begin with % followed by a character that represents the data type. %s is a placeholder for a string. The first %s is replaced with the value found in the \u201cContact Name as Plaintext\u201d column which is a hidden column and the second %s is replaced by the value in the Team column. \\n adds a new line.","title":"Examples using Method 2"},{"location":"formula-cheat-sheet/#splitting-strings-of-text","text":"Split a string using Python\u2019s split() method. If Full Name is George Washington , $Full_Name.split(\" \") would return [George, Washington] .","title":"Splitting Strings of Text"},{"location":"formula-cheat-sheet/#example-of-splitting-strings-of-text","text":"Community Example: Colors The formula in the \u201cColor Reference (Just URL)\u201d column of Table 2 is: split = $Color_Reference.Color.split(\" \") return split[-1] $Color_Reference.Color uses the reference column, \u201cColor Reference\u201d to pull data from the table it is referencing, Table 1. Specifically, it pulls the value from the Color column of Table 1. Color is a text column that contains a hyperlink with a label. We only see the label in Table 1 but as you can see in the screenshot above, the value in the \u2018pink\u2019 cell is expanded to show the entire string which contains \u201cpink\u201d followed by the URL. You can also see this in the \u201cColor Reference\u201d column of Table 2. We want to get the link by itself in \u201cColor Reference (Just URL)\u201d. We can do this using Python\u2019s split() method. .split(\" \") allows us to split the string anywhere there is a space (\" \") . In the Color column, there is a label followed by a space followed by the URL. The value from the Color column is split into a list containing two items Label and URL . This list is assigned to the variable split . We want to return the last item in the list split in order to get our URL . The last item in a list always has index [-1] . return split[-1] returns the last item in the list split .","title":"Example of Splitting Strings of Text"},{"location":"formula-cheat-sheet/#direct-link-to-gmail-history-for-a-contact","text":"If you store contacts in Grist, and use Gmail to email them, you can create a formula that will open Gmail to a list of conversations with that contact. Read about it in the Community: Pull up Gmail history for a particular contact","title":"Direct Link to Gmail History for a Contact"},{"location":"formula-cheat-sheet/#troubleshooting_1","text":"Is your URL still showing after you added a label? Make sure your Column Type is Text and Cell Format is Hyperlink.","title":"Troubleshooting"},{"location":"formula-cheat-sheet/#joining-a-list-of-strings","text":"When you want to join a list of strings, you can use Python\u2019s join() method .","title":"Joining a List of Strings"},{"location":"formula-cheat-sheet/#example-of-joining-a-list","text":"Community Example: .join() Example The formula used in the Advertisement column of the 2022 Grand Openings table is: \"Coming soon to a city near you!\\n\" + \" : \".join($New_Location_s_in_2022) Here, we are joining multiple strings to create our advertisement. \"Coming soon to a city near you!\\n\" is returned almost exactly as we see it, minus the quotes \"\" and \\n at the end of the string. The quotes \"\" specify that this is a string and \\n is actually a newline character that can be used to specify a new line within a string. \" : \".join($New_Location_s_in_2022) is also a string but uses Python\u2019s join() method to join values from our choice list column, \u201cNew Locations in 2022\u201d. What we see in quotes before .join is what will separate each value in our list. In this example, each value is separated by a space, : and another space.","title":"Example of Joining a List"},{"location":"formula-cheat-sheet/#finding-duplicates","text":"You can find duplicates in a column using either conditional formatting or a helper column. Read about it in the Community: Ensure unique values or detect duplicates","title":"Finding Duplicates"},{"location":"formula-cheat-sheet/#example-of-finding-duplicates","text":"Community Example: Finding Duplicates The formula used in the Duplicate? column of the Duplicates table is: len(Duplicates.lookupRecords(Grocery_List=$Grocery_List))>1 Let\u2019s break this down, working from the inside > out. Duplicates.lookupRecords(Grocery_List=$Grocery_List) This is a lookupRecords function that follows the format of: [Table_Name].lookupRecords([A]=$[B]) Where [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. This formula looks up records in the Duplicates table where a record in the Grocery List column matches another record in the same column. len() counts the number of records in our list. Since each duplicate will match with the other, it should appear twice in our list. This is why len() > 1 . if len() > 1 , the formula is true. If len() <= 1 , the formula is false. This same formula can be used in conditional formatting. This can be seen in the \u2018Grocery List\u2019 column of the Duplicates table. If len() > 1 , our formula is true and the conditional cell color is applied to these cells. If len() <= 1 , our formula is false and the cell color is unchanged.","title":"Example of Finding Duplicates"},{"location":"formula-cheat-sheet/#using-a-records-unique-identifier-in-formulas","text":"When a record is created, it is assigned a numeric id (available as $id in formulas) that is unique within that table. You can reveal the row ID by adding a formula column where the formula is $id .","title":"Using a Record's Unique Identifier in Formulas"},{"location":"formula-cheat-sheet/#examples-using-row-id-in-formulas","text":"You can reveal the ID with the formula $id Custom Product Builder template The formula used in the Contract No. column of the Contract Builder table is: $id + 500 Here, we are using a trigger formula to create a unique Contract Number when a record is created. Class Enrollment template The formula used in the Count column of the Classes table is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This is refered to as a Reverse Lookup. We can use the row id to match a record in another table where a reference column is used. LookupRecords follows the format [Table_Name].lookupRecords([A]=$[B]) . [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Lookup Records creates a list of records that match the criteria listed. len() counts how many records are in that list. Here, we are looking up records from the Enrollments table where the record called out in the Class column (our reference column) has the same row ID as the row in the table you are entering the formula. Additionally, the value in the Status column of the Enrollments table is Confirmed . We\u2019ll walk through this. The table we are looking up records in is the Enrollments table. Our criteria comes from the Class column and the Status column.The criteria for Status is straightforward; the value must be Confirmed in order to be included in our list of records. Class is a bit more complicated. As we see in the screenshot below, Class is a reference column that pulls data from the Classes table. Here, the Class column shows 2018F-Stars . A reference column points to the entire record, not just the value you see here in the Class column. Using the configuration panel on the right hand side of the screen, you can pick any column from the originating table to show. For this example, the Class column shows the value from the Class Code column of the Classes table but it points to the entire record where the class code is 2018F-Stars . As you can see in this screenshot, the Row ID for this particular record is 2 and because we are calculating the Count for the row with Row ID = 2 , it will count all records in the Enrollment table where Class shows 2018F-Stars and Status is Confirmed . Restaurant Custom Orders template The trigger formula used in the BOM # column of the Bill of Materials table is: MAX(o.BOM_ for o in Bill_Of_Materials.all if o.id != $id) + 1 First, we\u2019ll walk through the formula inside the parenthesis then work outwards. Here, o is a variable representing each record in our table. o.BOM_ represents the BOM # for each record and o.id represents the row ID for each record. This is a for loop that makes a list of the BOM # for each record in the table Bill of Materials when the record ID does not equal the ID of this row. MAX() finds the maximum BOM # in the list then + 1 to get our final value. This is a trigger formula that only applies to new records. When a new record is created, the formula finds the highest BOM # in the table then adds 1 so we have a unique BOM # for the new record.","title":"Examples Using Row ID in Formulas"},{"location":"formula-cheat-sheet/#removing-duplicates-from-a-list","text":"You can remove duplicates from a list with help from Python\u2019s set() method.","title":"Removing Duplicates From a List"},{"location":"formula-cheat-sheet/#example-of-removing-duplicates-from-a-list","text":"Community Example: Removing Duplicates From a List The formula in the All Divisions column of the Abroad Trips table is: confirmed_div = $Attending_Confirmed.Role_Division.Division pending_div = $Attending_Pending.Role_Division.Division full_list = confirmed_div + pending_div return sorted(set(full_list)) We will walk through this one line at a time. Attending-Confirmed is a Reference List column that pulls data from the EMPLOYEES table. $Attending_Confirmed.Role_Division pulls the value from the Role Division column of the EMPLOYEES table. The Role Division column in the EMPLOYEES table is a reference column itself, which points to a record in the Divisions table. Chaining allows us to specify what information we want from this record. In this case, we want the Division. We expand our formula to $Attending_Confirmed.Role_Division.Division . The Division is found for each employee listed in the Attending-Confirmed column, creating a list. We assign this list of divisions to the variable confirmed_div . Attending-Pending is also a Reference List column that pulls data from the EMPLOYEES table. $Attending_Pending.Role_Division.Division does the same as above except now we pull the division for each employee in the Attending-Pending column. We assign this list to the variable pending_div . We create a list of all divisions by adding the two lists together and assigning this combined list to the variable full_list . return sorted(set(full_list)) first uses Python\u2019s set() method to create a list with no duplicate divisions. We then use the sorted() method to return our set of divisions, sorted alphabetically. Note that the formula above can be simplified even further to: sorted( set($Attending_Confirmed.Role_Division.Division) | set($Attending_Pending.Role_Division.Division) )","title":"Example of Removing Duplicates from a List"},{"location":"formula-cheat-sheet/#setting-default-values-for-new-records","text":"You can set default values for when a new record is created and save yourself the trouble of having to fill in the same fields with the same values time after time. Read about it in the Community: Default values on the widget","title":"Setting Default Values for New Records"},{"location":"formula-cheat-sheet/#working-with-dates-and-times","text":"","title":"Working with dates and times"},{"location":"formula-cheat-sheet/#automatic-date-time-and-author-stamps","text":"You can automatically add the date or time a record was created or updated as well as who made the change.","title":"Automatic Date, Time and Author Stamps"},{"location":"formula-cheat-sheet/#examples-of-automatic-date-time-and-author-stamps","text":"Grant Application Tracker template The formula used in the Last Updated column of the Tasks table is: NOW() This is a trigger formula that triggers when a change is made to any field for this record. When a change is made, this formula runs its calculation. NOW() calculates the current time and date for the time zone selected. The formula used in the Created By column of the Tasks table is: user.Name This is a trigger formula that triggers when a new record is created. When the record is created, this formula runs its calculation. user.Name looks up the user account that is logged into Grist and returns the name associated with that account.","title":"Examples of Automatic Date, Time and Author Stamps"},{"location":"formula-cheat-sheet/#troubleshooting-errors_2","text":"If the time value in your datetime column is not calculating, check your formula. If TODAY() is used in DateTime, the time will always show 12:00am as you see below. NOW() is used for DateTime columns. TODAY() is used for Date. #AttributeError You have likely entered user.name but the formula is user.Name . Keep an eye on capitalization! #NameError You may have entered username or userName . The correct formula is user.Name . Another possibility is that this was entered in as a Formula column rather than a trigger formula column. Convert it to a trigger formula and this should resolve the problem.","title":"Troubleshooting Errors"},{"location":"formula-cheat-sheet/#filtering-data-within-a-specified-amount-of-time","text":"Using the DATEADD() function and comparision operators , you can determine if a date falls within a specific range then apply a filter.","title":"Filtering Data within a Specified Amount of Time"},{"location":"formula-cheat-sheet/#example-filtering-data-that-falls-in-1-month-range","text":"Community Example: Filtering Data Within a 1-Month Range The formula used in the Falls in 1 Month Range? column of the Interactions table is: TODAY() >= $Date >= DATEADD(TODAY(),months=-1) TODAY() returns the current date. $Date is the name of a column in our table, which is a Date type column. DATEADD(start_date, days=0, months=0, years=0, weeks=0) returns the date that is the given number of days, months, years, or weeks before or after the start_date . In this example, it returns the date that is one month prior to the start date, TODAY() . This formula is true if the date value in the Date column falls between TODAY() and our DATEADD() date which is one month ago. If the date value in the Date column does not fall between these two dates, the formula returns false. We can use this column to filter our data. If we only want to see interactions that fall within the 1 Month Range, we would filter to only include true values. If we want to see interactions that fall outside of the 1 Month Range, we would filter to only include false values.","title":"Example Filtering Data that 'Falls in 1 Month Range`"},{"location":"formula-cheat-sheet/#troubleshooting-errors_3","text":"#TypeError : Because $Date is a Date type column, TODAY() must be used in formulas comparing dates. NOW() is a DateTime formula that should only be used with other DateTime values. For example, if the $Date column was a DateTime type column, NOW() would need to be used rather than TODAY() because it includes the time component. NOW() is date and time. TODAY() is only date.","title":"Troubleshooting Errors"},{"location":"formula-timer/","text":"Formula timer # Grist has a built-in formula timer that will measure the time it takes to evaluate each formula in a document. This helps diagnose which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes. Grist\u2019s formula timer can be found on the \u2018Document Settings\u2019 page, under \u2018Data Engine\u2019. Select \u2018Start timing\u2019 to begin. On the next screen, you will have two choices, \u2018Start timing\u2019 and \u2018Time reload\u2019. Start timing \u2018Start timing\u2019 allows you to make changes to the document then stop timing to see the results. This is useful if you want to test specific formulas. You can make a change that affects that formula then come back and click \u2018Stop timing\u2019 to see the result. Time reload \u2018Time reload\u2019 forces a reload of the document while timing formulas and shows the result. This will show timing results for all formulas across the entire document. Results # Results are displayed in a table format. \u26a0\ufe0f Results Table The Formula Timer results table is not saved anywhere in the document. If you click away from this page, you will need to run the formula timer again to retrieve the table. Sort the Total Time column from Z > A so the formulas that take the longest time to run are listed first. The table specifies the Table ID and Column ID containing each formula. Review the formulas with the highest total time to see how they can be improved. If your document is experiencing slowness due to formula calculations, you\u2019ll see Total Times greater than 1 second. Need guidance on how to improve a formula? Post to our Community Forum !","title":"Formula timer"},{"location":"formula-timer/#formula-timer","text":"Grist has a built-in formula timer that will measure the time it takes to evaluate each formula in a document. This helps diagnose which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes. Grist\u2019s formula timer can be found on the \u2018Document Settings\u2019 page, under \u2018Data Engine\u2019. Select \u2018Start timing\u2019 to begin. On the next screen, you will have two choices, \u2018Start timing\u2019 and \u2018Time reload\u2019. Start timing \u2018Start timing\u2019 allows you to make changes to the document then stop timing to see the results. This is useful if you want to test specific formulas. You can make a change that affects that formula then come back and click \u2018Stop timing\u2019 to see the result. Time reload \u2018Time reload\u2019 forces a reload of the document while timing formulas and shows the result. This will show timing results for all formulas across the entire document.","title":"Formula timer"},{"location":"formula-timer/#results","text":"Results are displayed in a table format. \u26a0\ufe0f Results Table The Formula Timer results table is not saved anywhere in the document. If you click away from this page, you will need to run the formula timer again to retrieve the table. Sort the Total Time column from Z > A so the formulas that take the longest time to run are listed first. The table specifies the Table ID and Column ID containing each formula. Review the formulas with the highest total time to see how they can be improved. If your document is experiencing slowness due to formula calculations, you\u2019ll see Total Times greater than 1 second. Need guidance on how to improve a formula? Post to our Community Forum !","title":"Results"},{"location":"formulas/","text":"Formulas # Grist has a powerful data engine to calculate the cells of your tables using formulas. It even has an AI Formula Assistant to help write formulas. If you\u2019ve used spreadsheets before, or database expressions, you\u2019ll be on familiar territory - but there are some wrinkles you\u2019ll want to know about, so hang around. Let\u2019s start with a classic use of spreadsheets. Suppose you have a list of products you\u2019ve ordered, the quantity you ordered, and the unit price of each. You\u2019ve made a column to show the quantity times the unit price, but want the computer to do that part for you. Just select a cell in the column you want to fill, and hit = key to tell Grist you want to enter a formula, rather than a value. Did you notice, when you did that, the labels of the columns changed a little? \u201cProduct\u201d became \u201c$Product\u201d, and \u201cUnit Price\u201d became \u201c$Unit_Price\u201d. This is Grist telling you how to refer to those columns in your formula. Just type $Quantity * $Unit_Price . You\u2019ll find an auto-complete feature ready to help you. Or if you don\u2019t like typing, click on the Quantity column, type the multiplication symbol, and then click on the Unit Price column. Your formula should look like this: To control the column ID, like \u201c$Unit_Price\u201d, that\u2019s used in formulas, see Renaming columns . Press Enter , and your formula is applied to all cells in the column. If you\u2019ve worked with spreadsheets before, you may be surprised that you don\u2019t need to specify row numbers, like B1 * C1 . In Grist, a single formula applies to a whole column. You don\u2019t have to worry about filling it in for all rows, and can refer to values in the same row without fuss. You can format numeric columns to look better by setting column type to Numeric , and selecting suitable formatting options: Column behavior # When we provide a formula for a column we tell Grist to update its value on every change in a document. We can no longer type a value into the cell, because its value is determined purely by the formula. A formula column is one of three possible column behaviors, which you can control using the COLUMN BEHAVIOR section in the creator panel: Data column maintains data, which you can manually update or clear, or optionally calculate using trigger formulas . Formula column always reflects the result of formula calculation, and is kept up-to-date by Grist. Empty column is a state for a new column. Typing any value into it will turn it into a Data Column, while typing in a formula will turn it into a Formula Column Using the COLUMN BEHAVIOR section, you can manually change the column behavior. The most common options are available as green action buttons at the bottom, and other options are available under the behavior menu. Depending on the current column behavior, those are: Set formula action converts an empty column to a formula column. Set trigger formula or Convert to trigger formula action sets a trigger on a column (more on triggers in the next Trigger formulas section ). Make into data column action converts an empty column to a regular data column. Convert column to data converts a formula column to regular data column (you can read more about this feature in the Freeze a formula column section ). Clear and make into formula clears all the data in a column and converts it to a formula column. (We say \u201cclear\u201d as a reminder that existing data in the column will be lost. They\u2019ll be replaced by the calculation results from the formula.) Clear and reset clears all data and completely resets the column to its initial Empty Column state. Python # Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Grist documents may use Python 2 or Python 3, see our Python guide for details. Formulas that operate over many rows # If you are a spreadsheet user, you may find yourself wanting to have some special rows at the end of your table that have formulas different to the rest. In Grist, we\u2019d like you to consider adding a widget to your page instead. For common use cases, Summary tables may be exactly what you need. Or if you want to set things up yourself, you can add an extra table widget like this (see Page widgets for details): This is just another table, giving us a place to put formulas outside of the structure of the Materials table. For example, if we wanted to count how many products there are in that table, we could use this formula: len(Materials.all) Every table in your document is available by its name in formulas, as a UserTable . This formula uses the all method to access the rows of the table, but doesn\u2019t do anything with them but count them. Here\u2019s a formula to compute the average price, using the Excel-like function AVERAGE : AVERAGE(Materials.all.Price) The all method returns a RecordSet , which supports iterating over individual columns this way. Equivalently, we could use a Python list comprehension : AVERAGE(material.Price for material in Materials.all) If you are not familiar with Python, it is worth following a tutorial. There are thousands online, including this official one . Python will be useful to you for all sorts of data work, not just Grist. List comprehension is useful once we\u2019re doing anything nuanced. For example, here\u2019s a formula to list the names of products with a quantity greater than 80: [m.Product for m in Materials.all if m.Quantity > 80] This is a list comprehension, but now with a conditional. The result is a list, which is rendered as text in a cell. Python can help in other ways in your search for rows. For example, here\u2019s a formula to find the name of the product with the highest quantity: max(Materials.all, key=lambda m: m.Quantity).Product Formulas are case-sensitive, with Excel-like functions being all-caps ( MAX ), and regular Python generally all lowercase ( max ). For exact matches, there is a shortcut to avoid iteration called lookupRecords , or lookupOne for single matches. Just pass the the values of columns you require to be matched. For example, here is a formula to look up the product name of a material with a quantity of 52: Materials.lookupOne(Quantity=52).Product For very large tables, it is wise to use lookups as much as you can, rather than iterating through rows. Returning to our example document, you can now see how we calculated the Total Spent , Average Quantity , and Most Ordered Product columns: Column Formula Total Spent SUM(Materials.all.Price) Average Quantity AVERAGE(Materials.all.Quantity) Most Ordered Product max(Materials.all, key=lambda m: m.Quantity).Product Separating out calculations like this from the body of your data can take some getting used to, but working this way can help keep your document more organized. And it brings other advantages. For example we could switch the formatting of the summary widget via the side panel: Varying formula by row # Having a formula apply to all rows is convenient and reduces the changes of mistakes. If you need to have a column change its behavior on different rows, it is possible using a conditional in the formula. For example, here is a replacement for the Materials.Price formula that ignores the price and shows zero for products whose name ends in \u201c(Sample)\u201d: if $Product.endswith(\"(Sample)\"): return 0 else: return $Quantity * $Unit_Price Code viewer # Once you have a lot of formulas, or if you have been invited to a document and want to get an overview of its formulas, there is a code viewer available with a pure Python summary of the document. Special values available in formulas # For those familiar with Python, here are the extra values available to you in Grist: rec is the current row. The $column syntax is shorthand for rec.column . The rec variable is of type Record . table is the current table, and is of type UserTable . Tables in your document are available by their name, and are also of type UserTable . Many extra spreadsheet functions are available, see the full function list . If your table or column has a space in its name, or other characters that are awkward in Python, those characters are replaced with an underscore. Auto-complete may help you if you\u2019re not sure. You can also control the \u201cids\u201d of columns and tables in the right side panel. Freeze a formula column # If you\u2019d like to save the output of your formula as plain values, you can simply change column behavior from Formula Column to Data Column . First open the column options in the side panel: Now click on the Formula Column and select Convert column to data option. Notice that there is no = sign in the column cells any more, showing that it is no longer a formula. The cells will no longer change if other cells they used to depend on change. The original formula is saved but stays inactive. It may come useful again if you wish to convert the column back to a formula column, or use it as a Trigger Formula . The side panel has lots of other handy settings, such as cell formatting (number of digits after decimal point, color, etc). The options apply just as much to formula columns as to regular columns. Lookups # Grist functions lookupOne and lookupRecords are useful for enumerating subsets of your data. For example, suppose we added a Category column to our Materials table, and wished to list all products belonging to a specific category. We can do this using TABLE.lookupRecords , where TABLE is the table of interest, and supplying it with the column values to match. For example, Materials.lookupRecords(Category='Ship') , as here: If you are following on, see Adding a field for details of how to add a new field to a card. If you care about the order of results, lookupRecords takes an optional sort_by parameter. For example, we could use this formula to sort by the product name itself: Materials.lookupRecords(Category='Ship', sort_by='Product').Product If you want to sort by multiple columns, remember that you can create a hidden formula column that combines data in any way you like, and then sort by that. The order of records returned by lookupRecords may not match the order of rows you see in a table. To get that order, use sort_by='manualSort' . This is an internal column that is updated with the manually established sort order of rows. If you find yourself doing a lot of look-ups, please consider whether Summary tables and Summary formulas might be what you are looking for. Recursion # Lookups are handy for recursive formulas. Suppose we have a table counting how many events we have per day, and want to add a cumulative sum of those event counts. One way to do that is with a formula like this: yesterday = Events.lookupOne(date=$date - datetime.timedelta(days=1)) $events + (yesterday.cumulative or 0) For clarity, we\u2019ve split this formula into two lines. The first line makes a variable pointing to the row of the day before. The second line computes the value we want in the cell. Python note: the value of the last line is automatically returned (you could prefix it with return if you like). Notice the yesterday.cumulative or 0 . For the earliest row in the table, there will be no yesterday. In this case, lookupOne returns a special empty record, for which yesterday.cumulative will be None . If you\u2019d like to simplify this formula, or find yourself using the same lookup in multiple formulas, it would be worth making yesterday a reference column . Simply add a reference column, and give a formula for it that matches how we defined yesterday here. To actually enter this formula in a cell, you\u2019d use Shift + Enter to divide the lines. Trigger Formulas # Formula columns are great for calculated values \u2013 those determined by other data in the document. It may also be useful to store independent data in a column, but still use a formula to calculate it in some situations. This is exactly what Trigger Formulas offer. It is a very powerful feature that allows you to create a Timestamp or Authorship column, recalculate your data based on a set of conditions that you decide , clean data when a new value is entered, or provide sensible default value for a column. To create a Trigger Formula column, you first need to open the creator panel and click on the Set trigger formula action. If you want to convert an existing formula, use the Convert to trigger formula action available in the COLUMN BEHAVIOR section. To control when the formula is evaluated, use the two checkbox options below: Apply to new records triggers the formula only when a new record is created (a default cell value). Apply on record changes triggers the formula when a record is updated. Applying to new records is self-explanatory, the formula will be evaluated only once when you add a new record. It is a perfect solution to provide default values to the empty cells. Second option allows you to fine grain the conditions and specify which columns, when updated, will trigger the evaluation: You probably noticed the first option Current field . At first glance, you probably wonder: \u201cWhy would I want to trigger the column on its own change?\u201d. This option allows you to react to a value that is being entered into the column, just before it is saved! In the formula editor, you have access to two variables that are not available to regular formulas: value which is the value that a user wants to enter, user which represents a user object that is making the change (you will also see this in the Access rules section). This allows you to make your application even smarter, track when a record was updated , or see who made the last change to a row . Simple examples: Ensure that the value in a column is always written in capital letters: With the trigger formula of value.upper() , the value typed into this column will be converted to upper case automatically. Format a value that the user enters to sanitize the data before saving: With the formula like value if value.startswith(\"SK\") else \"SK\" + value , the value typed into this column will always be prefixed with \u201cSK\u201d. Overwrite a default value from a referenced table: You can use a formula like value or $Client.Phone , to provide a default value from a referenced table, but still allow the user to type a new one. In each of these examples, when the user tries to modify a cell, Grist (before updating the record) will evaluate the formula and store its result in the column instead of the value provided by the user. For a detailed, real-life example read our guide on how to create time and user stamps. For more information on formulas and trigger formulas, check out our webinar Trigger Formulas v. Formulas .","title":"Intro to formulas"},{"location":"formulas/#formulas","text":"Grist has a powerful data engine to calculate the cells of your tables using formulas. It even has an AI Formula Assistant to help write formulas. If you\u2019ve used spreadsheets before, or database expressions, you\u2019ll be on familiar territory - but there are some wrinkles you\u2019ll want to know about, so hang around. Let\u2019s start with a classic use of spreadsheets. Suppose you have a list of products you\u2019ve ordered, the quantity you ordered, and the unit price of each. You\u2019ve made a column to show the quantity times the unit price, but want the computer to do that part for you. Just select a cell in the column you want to fill, and hit = key to tell Grist you want to enter a formula, rather than a value. Did you notice, when you did that, the labels of the columns changed a little? \u201cProduct\u201d became \u201c$Product\u201d, and \u201cUnit Price\u201d became \u201c$Unit_Price\u201d. This is Grist telling you how to refer to those columns in your formula. Just type $Quantity * $Unit_Price . You\u2019ll find an auto-complete feature ready to help you. Or if you don\u2019t like typing, click on the Quantity column, type the multiplication symbol, and then click on the Unit Price column. Your formula should look like this: To control the column ID, like \u201c$Unit_Price\u201d, that\u2019s used in formulas, see Renaming columns . Press Enter , and your formula is applied to all cells in the column. If you\u2019ve worked with spreadsheets before, you may be surprised that you don\u2019t need to specify row numbers, like B1 * C1 . In Grist, a single formula applies to a whole column. You don\u2019t have to worry about filling it in for all rows, and can refer to values in the same row without fuss. You can format numeric columns to look better by setting column type to Numeric , and selecting suitable formatting options:","title":"Formulas"},{"location":"formulas/#column-behavior","text":"When we provide a formula for a column we tell Grist to update its value on every change in a document. We can no longer type a value into the cell, because its value is determined purely by the formula. A formula column is one of three possible column behaviors, which you can control using the COLUMN BEHAVIOR section in the creator panel: Data column maintains data, which you can manually update or clear, or optionally calculate using trigger formulas . Formula column always reflects the result of formula calculation, and is kept up-to-date by Grist. Empty column is a state for a new column. Typing any value into it will turn it into a Data Column, while typing in a formula will turn it into a Formula Column Using the COLUMN BEHAVIOR section, you can manually change the column behavior. The most common options are available as green action buttons at the bottom, and other options are available under the behavior menu. Depending on the current column behavior, those are: Set formula action converts an empty column to a formula column. Set trigger formula or Convert to trigger formula action sets a trigger on a column (more on triggers in the next Trigger formulas section ). Make into data column action converts an empty column to a regular data column. Convert column to data converts a formula column to regular data column (you can read more about this feature in the Freeze a formula column section ). Clear and make into formula clears all the data in a column and converts it to a formula column. (We say \u201cclear\u201d as a reminder that existing data in the column will be lost. They\u2019ll be replaced by the calculation results from the formula.) Clear and reset clears all data and completely resets the column to its initial Empty Column state.","title":"Column behavior"},{"location":"formulas/#python","text":"Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Grist documents may use Python 2 or Python 3, see our Python guide for details.","title":"Python"},{"location":"formulas/#formulas-that-operate-over-many-rows","text":"If you are a spreadsheet user, you may find yourself wanting to have some special rows at the end of your table that have formulas different to the rest. In Grist, we\u2019d like you to consider adding a widget to your page instead. For common use cases, Summary tables may be exactly what you need. Or if you want to set things up yourself, you can add an extra table widget like this (see Page widgets for details): This is just another table, giving us a place to put formulas outside of the structure of the Materials table. For example, if we wanted to count how many products there are in that table, we could use this formula: len(Materials.all) Every table in your document is available by its name in formulas, as a UserTable . This formula uses the all method to access the rows of the table, but doesn\u2019t do anything with them but count them. Here\u2019s a formula to compute the average price, using the Excel-like function AVERAGE : AVERAGE(Materials.all.Price) The all method returns a RecordSet , which supports iterating over individual columns this way. Equivalently, we could use a Python list comprehension : AVERAGE(material.Price for material in Materials.all) If you are not familiar with Python, it is worth following a tutorial. There are thousands online, including this official one . Python will be useful to you for all sorts of data work, not just Grist. List comprehension is useful once we\u2019re doing anything nuanced. For example, here\u2019s a formula to list the names of products with a quantity greater than 80: [m.Product for m in Materials.all if m.Quantity > 80] This is a list comprehension, but now with a conditional. The result is a list, which is rendered as text in a cell. Python can help in other ways in your search for rows. For example, here\u2019s a formula to find the name of the product with the highest quantity: max(Materials.all, key=lambda m: m.Quantity).Product Formulas are case-sensitive, with Excel-like functions being all-caps ( MAX ), and regular Python generally all lowercase ( max ). For exact matches, there is a shortcut to avoid iteration called lookupRecords , or lookupOne for single matches. Just pass the the values of columns you require to be matched. For example, here is a formula to look up the product name of a material with a quantity of 52: Materials.lookupOne(Quantity=52).Product For very large tables, it is wise to use lookups as much as you can, rather than iterating through rows. Returning to our example document, you can now see how we calculated the Total Spent , Average Quantity , and Most Ordered Product columns: Column Formula Total Spent SUM(Materials.all.Price) Average Quantity AVERAGE(Materials.all.Quantity) Most Ordered Product max(Materials.all, key=lambda m: m.Quantity).Product Separating out calculations like this from the body of your data can take some getting used to, but working this way can help keep your document more organized. And it brings other advantages. For example we could switch the formatting of the summary widget via the side panel:","title":"Formulas that operate over many rows"},{"location":"formulas/#varying-formula-by-row","text":"Having a formula apply to all rows is convenient and reduces the changes of mistakes. If you need to have a column change its behavior on different rows, it is possible using a conditional in the formula. For example, here is a replacement for the Materials.Price formula that ignores the price and shows zero for products whose name ends in \u201c(Sample)\u201d: if $Product.endswith(\"(Sample)\"): return 0 else: return $Quantity * $Unit_Price","title":"Varying formula by row"},{"location":"formulas/#code-viewer","text":"Once you have a lot of formulas, or if you have been invited to a document and want to get an overview of its formulas, there is a code viewer available with a pure Python summary of the document.","title":"Code viewer"},{"location":"formulas/#special-values-available-in-formulas","text":"For those familiar with Python, here are the extra values available to you in Grist: rec is the current row. The $column syntax is shorthand for rec.column . The rec variable is of type Record . table is the current table, and is of type UserTable . Tables in your document are available by their name, and are also of type UserTable . Many extra spreadsheet functions are available, see the full function list . If your table or column has a space in its name, or other characters that are awkward in Python, those characters are replaced with an underscore. Auto-complete may help you if you\u2019re not sure. You can also control the \u201cids\u201d of columns and tables in the right side panel.","title":"Special values available in formulas"},{"location":"formulas/#freeze-a-formula-column","text":"If you\u2019d like to save the output of your formula as plain values, you can simply change column behavior from Formula Column to Data Column . First open the column options in the side panel: Now click on the Formula Column and select Convert column to data option. Notice that there is no = sign in the column cells any more, showing that it is no longer a formula. The cells will no longer change if other cells they used to depend on change. The original formula is saved but stays inactive. It may come useful again if you wish to convert the column back to a formula column, or use it as a Trigger Formula . The side panel has lots of other handy settings, such as cell formatting (number of digits after decimal point, color, etc). The options apply just as much to formula columns as to regular columns.","title":"Freeze a formula column"},{"location":"formulas/#lookups","text":"Grist functions lookupOne and lookupRecords are useful for enumerating subsets of your data. For example, suppose we added a Category column to our Materials table, and wished to list all products belonging to a specific category. We can do this using TABLE.lookupRecords , where TABLE is the table of interest, and supplying it with the column values to match. For example, Materials.lookupRecords(Category='Ship') , as here: If you are following on, see Adding a field for details of how to add a new field to a card. If you care about the order of results, lookupRecords takes an optional sort_by parameter. For example, we could use this formula to sort by the product name itself: Materials.lookupRecords(Category='Ship', sort_by='Product').Product If you want to sort by multiple columns, remember that you can create a hidden formula column that combines data in any way you like, and then sort by that. The order of records returned by lookupRecords may not match the order of rows you see in a table. To get that order, use sort_by='manualSort' . This is an internal column that is updated with the manually established sort order of rows. If you find yourself doing a lot of look-ups, please consider whether Summary tables and Summary formulas might be what you are looking for.","title":"Lookups"},{"location":"formulas/#recursion","text":"Lookups are handy for recursive formulas. Suppose we have a table counting how many events we have per day, and want to add a cumulative sum of those event counts. One way to do that is with a formula like this: yesterday = Events.lookupOne(date=$date - datetime.timedelta(days=1)) $events + (yesterday.cumulative or 0) For clarity, we\u2019ve split this formula into two lines. The first line makes a variable pointing to the row of the day before. The second line computes the value we want in the cell. Python note: the value of the last line is automatically returned (you could prefix it with return if you like). Notice the yesterday.cumulative or 0 . For the earliest row in the table, there will be no yesterday. In this case, lookupOne returns a special empty record, for which yesterday.cumulative will be None . If you\u2019d like to simplify this formula, or find yourself using the same lookup in multiple formulas, it would be worth making yesterday a reference column . Simply add a reference column, and give a formula for it that matches how we defined yesterday here. To actually enter this formula in a cell, you\u2019d use Shift + Enter to divide the lines.","title":"Recursion"},{"location":"formulas/#trigger-formulas","text":"Formula columns are great for calculated values \u2013 those determined by other data in the document. It may also be useful to store independent data in a column, but still use a formula to calculate it in some situations. This is exactly what Trigger Formulas offer. It is a very powerful feature that allows you to create a Timestamp or Authorship column, recalculate your data based on a set of conditions that you decide , clean data when a new value is entered, or provide sensible default value for a column. To create a Trigger Formula column, you first need to open the creator panel and click on the Set trigger formula action. If you want to convert an existing formula, use the Convert to trigger formula action available in the COLUMN BEHAVIOR section. To control when the formula is evaluated, use the two checkbox options below: Apply to new records triggers the formula only when a new record is created (a default cell value). Apply on record changes triggers the formula when a record is updated. Applying to new records is self-explanatory, the formula will be evaluated only once when you add a new record. It is a perfect solution to provide default values to the empty cells. Second option allows you to fine grain the conditions and specify which columns, when updated, will trigger the evaluation: You probably noticed the first option Current field . At first glance, you probably wonder: \u201cWhy would I want to trigger the column on its own change?\u201d. This option allows you to react to a value that is being entered into the column, just before it is saved! In the formula editor, you have access to two variables that are not available to regular formulas: value which is the value that a user wants to enter, user which represents a user object that is making the change (you will also see this in the Access rules section). This allows you to make your application even smarter, track when a record was updated , or see who made the last change to a row . Simple examples: Ensure that the value in a column is always written in capital letters: With the trigger formula of value.upper() , the value typed into this column will be converted to upper case automatically. Format a value that the user enters to sanitize the data before saving: With the formula like value if value.startswith(\"SK\") else \"SK\" + value , the value typed into this column will always be prefixed with \u201cSK\u201d. Overwrite a default value from a referenced table: You can use a formula like value or $Client.Phone , to provide a default value from a referenced table, but still allow the user to type a new one. In each of these examples, when the user tries to modify a cell, Grist (before updating the record) will evaluate the formula and store its result in the column instead of the value provided by the user. For a detailed, real-life example read our guide on how to create time and user stamps. For more information on formulas and trigger formulas, check out our webinar Trigger Formulas v. Formulas .","title":"Trigger Formulas"},{"location":"functions/","text":"Function Reference # Grist formulas support most Excel functions, as well as the Python programming language. The table below lists Grist-specific functions, and the suite of the included Excel-like functions. In addition, the entire Python standard library is available. For more about using formulas in Grist, see Intro to Formulas . Grist uses Python (version 3.11) for formulas. You can use nearly all features of Python (see Python documentation ). Here are some helpful notes: Python is case-sensitive, including for Grist table and column names. Excel-like functions are always in uppercase. E.g. if is a Python keyword, while IF is an Excel-like function. Compare for equality using == , in place of Excel\u2019s single = (which in Python means assignment). \u201cNot equal\u201d uses != in place of Excel\u2019s <> . You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. Category Functions Grist Record or rec , $Field or rec.Field , $group or rec.group , RecordSet , UserTable , all , lookupOne , lookupRecords Date DATE , DATEADD , DATEDIF , DATEVALUE , DATE_TO_XL , DAY , DAYS , DTIME , EDATE , EOMONTH , HOUR , ISOWEEKNUM , MINUTE , MONTH , NOW , SECOND , TODAY , WEEKDAY , WEEKNUM , XL_TO_DATE , YEAR , YEARFRAC Info CELL , CURRENT_CONVERSION , ISBLANK , ISEMAIL , ISERR , ISERROR , ISLOGICAL , ISNA , ISNONTEXT , ISNUMBER , ISREF , ISREFLIST , ISTEXT , ISURL , N , NA , PEEK , RECORD , REQUEST , TYPE Logical AND , FALSE , IF , IFERROR , NOT , OR , TRUE Lookup lookupOne , lookupRecords , ADDRESS , CHOOSE , COLUMN , COLUMNS , CONTAINS , GETPIVOTDATA , HLOOKUP , HYPERLINK , INDEX , INDIRECT , LOOKUP , MATCH , OFFSET , ROW , ROWS , SELF_HYPERLINK , VLOOKUP Math ABS , ACOS , ACOSH , ARABIC , ASIN , ASINH , ATAN , ATAN2 , ATANH , CEILING , COMBIN , COS , COSH , DEGREES , EVEN , EXP , FACT , FACTDOUBLE , FLOOR , GCD , INT , LCM , LN , LOG , LOG10 , MOD , MROUND , MULTINOMIAL , ODD , PI , POWER , PRODUCT , QUOTIENT , RADIANS , RAND , RANDBETWEEN , ROMAN , ROUND , ROUNDDOWN , ROUNDUP , SERIESSUM , SIGN , SIN , SINH , SQRT , SQRTPI , SUBTOTAL , SUM , SUMIF , SUMIFS , SUMPRODUCT , SUMSQ , TAN , TANH , TRUNC , UUID Schedule SCHEDULE Stats AVEDEV , AVERAGE , AVERAGEA , AVERAGEIF , AVERAGEIFS , AVERAGE_WEIGHTED , BINOMDIST , CONFIDENCE , CORREL , COUNT , COUNTA , COVAR , CRITBINOM , DEVSQ , EXPONDIST , FDIST , FISHER , FISHERINV , FORECAST , F_DIST , F_DIST_RT , GEOMEAN , HARMEAN , HYPGEOMDIST , INTERCEPT , KURT , LARGE , LOGINV , LOGNORMDIST , MAX , MAXA , MEDIAN , MIN , MINA , MODE , NEGBINOMDIST , NORMDIST , NORMINV , NORMSDIST , NORMSINV , PEARSON , PERCENTILE , PERCENTRANK , PERCENTRANK_EXC , PERCENTRANK_INC , PERMUT , POISSON , PROB , QUARTILE , RANK , RANK_AVG , RANK_EQ , RSQ , SKEW , SLOPE , SMALL , STANDARDIZE , STDEV , STDEVA , STDEVP , STDEVPA , STEYX , TDIST , TINV , TRIMMEAN , TTEST , T_INV , T_INV_2T , VAR , VARA , VARP , VARPA , WEIBULL , ZTEST Text CHAR , CLEAN , CODE , CONCAT , CONCATENATE , DOLLAR , EXACT , FIND , FIXED , LEFT , LEN , LOWER , MID , PHONE_FORMAT , PROPER , REGEXEXTRACT , REGEXMATCH , REGEXREPLACE , REPLACE , REPT , RIGHT , SEARCH , SUBSTITUTE , T , TEXT , TRIM , UPPER , VALUE Grist # Record # class Record # A Record represents a record of data. It is the primary means of accessing values in formulas. A Record for a particular table has a property for each data and formula column in the table. In a formula, $field is translated to rec.field , where rec is the Record for which the formula is being evaluated. For example: def Full_Name(rec, table): return rec.First_Name + ' ' + rec.LastName def Name_Length(rec, table): return len(rec.Full_Name) $Field # $ Field or rec .Field # Access the field named \u201cField\u201d of the current record. E.g. $First_Name or rec.First_Name . $group # $group # In a summary table , $group is a special field containing the list of Records that are summarized by the current summary line. E.g. the formula len($group) counts the number of those records being summarized in each row. See RecordSet for useful properties offered by the returned object. Examples: sum($group.Amount) # Sum of the Amount field in the matching records sum(r.Amount for r in $group) # Same as sum($group.Amount) sum(r.Amount for r in $group if r > 0) # Sum of only the positive amounts sum(r.Shares * r.Price for r in $group) # Sum of shares * price products RecordSet # class RecordSet # A RecordSet represents a collection of records, as returned by Table.lookupRecords() or $group property in summary views. A RecordSet allows iterating through the records: sum(r.Amount for r in Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\")) min(r.DueDate for r in Tasks.lookupRecords(Owner=\"Bob\")) RecordSets also provide a convenient way to access the list of values for a particular field for all the records, as record_set.Field . For example, the examples above are equivalent to: sum(Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\").Amount) min(Tasks.lookupRecords(Owner=\"Bob\").DueDate) You can get the number of records in a RecordSet using len , e.g. len($group) . UserTable # class UserTable # Each data table in the document is represented in the code by an instance of UserTable class. These names are always capitalized. A UserTable provides access to all the records in the table, as well as methods to look up particular records. Every table in the document is available to all formulas. all # UserTable. all # The list of all the records in this table. For example, this evaluates to the number of records in the table Students . len(Students.all) This evaluates to the sum of the Population field for every record in the table Countries . sum(r.Population for r in Countries.all) lookupOne # UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. If sort_by=field is given, sort the results by that field. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email) People.lookupOne(Email=$Work_Email, sort_by=\"Date\") lookupRecords # UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. Date # DATE # DATE (year, month, day) # Returns the datetime.datetime object that represents a particular date. The DATE function is most useful in formulas where year, month, and day are formulas, not constants. If year is between 0 and 1899 (inclusive), adds 1900 to calculate the year. >>> DATE(108, 1, 2) datetime.date(2008, 1, 2) >>> DATE(2008, 1, 2) datetime.date(2008, 1, 2) If month is greater than 12, rolls into the following year. >>> DATE(2008, 14, 2) datetime.date(2009, 2, 2) If month is less than 1, subtracts that many months plus 1, from the first month in the year. >>> DATE(2008, -3, 2) datetime.date(2007, 9, 2) If day is greater than the number of days in the given month, rolls into the following months. >>> DATE(2008, 1, 35) datetime.date(2008, 2, 4) If day is less than 1, subtracts that many days plus 1, from the first day of the given month. >>> DATE(2008, 1, -15) datetime.date(2007, 12, 16) DATEADD # DATEADD (start_date, days=0, months=0, years=0, weeks=0) # Returns the date a given number of days, months, years, or weeks away from start_date . You may specify arguments in any order if you specify argument names. Use negative values to subtract. For example, DATEADD(date, 1) is the same as DATEADD(date, days=1) , ands adds one day to date . DATEADD(date, years=1, days=-1) adds one year minus one day. >>> DATEADD(DATE(2011, 1, 15), 1) datetime.date(2011, 1, 16) >>> DATEADD(DATE(2011, 1, 15), months=1, days=-1) datetime.date(2011, 2, 14) >>> DATEADD(DATE(2011, 1, 15), years=-2, months=1, days=3, weeks=2) datetime.date(2009, 3, 4) >>> DATEADD(DATE(1975, 4, 30), years=50, weeks=-5) datetime.date(2025, 3, 26) DATEDIF # DATEDIF (start_date, end_date, unit) # Calculates the number of days, months, or years between two dates. Unit indicates the type of information that you want returned: \u201cY\u201d: The number of complete years in the period. \u201cM\u201d: The number of complete months in the period. \u201cD\u201d: The number of days in the period. \u201cMD\u201d: The difference between the days in start_date and end_date. The months and years of the dates are ignored. \u201cYM\u201d: The difference between the months in start_date and end_date. The days and years of the dates are ignored. \u201cYD\u201d: The difference between the days of start_date and end_date. The years of the dates are ignored. Two complete years in the period (2) >>> DATEDIF(DATE(2001, 1, 1), DATE(2003, 1, 1), \"Y\") 2 440 days between June 1, 2001, and August 15, 2002 (440) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"D\") 440 75 days between June 1 and August 15, ignoring the years of the dates (75) >>> DATEDIF(DATE(2001, 6, 1), DATE(2012, 8, 15), \"YD\") 75 The difference between 1 and 15, ignoring the months and the years of the dates (14) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"MD\") 14 DATEVALUE # DATEVALUE (date_string, tz=None) # Converts a date that is stored as text to a datetime object. >>> DATEVALUE(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"30-Jan-2008\") datetime.datetime(2008, 1, 30, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"2008-12-11\") datetime.datetime(2008, 12, 11, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"5-JUL\").replace(year=2000) datetime.datetime(2000, 7, 5, 0, 0, tzinfo=moment.tzinfo('America/New_York')) In case of ambiguity, prefer M/D/Y format. >>> DATEVALUE(\"1/2/3\") datetime.datetime(2003, 1, 2, 0, 0, tzinfo=moment.tzinfo('America/New_York')) DATE_TO_XL # DATE_TO_XL (date_value) # Converts a Python date or datetime object to the serial number as used by Excel, with December 30, 1899 as serial number 1. See XL_TO_DATE for more explanation. >>> DATE_TO_XL(datetime.date(2008, 1, 1)) 39448.0 >>> DATE_TO_XL(datetime.date(2012, 3, 14)) 40982.0 >>> DATE_TO_XL(datetime.datetime(2012, 3, 14, 1, 30)) 40982.0625 DAY # DAY (date) # Returns the day of a date, as an integer ranging from 1 to 31. Same as date.day . >>> DAY(DATE(2011, 4, 15)) 15 >>> DAY(\"5/31/2012\") 31 >>> DAY(datetime.datetime(1900, 1, 1)) 1 DAYS # DAYS (end_date, start_date) # Returns the number of days between two dates. Same as (end_date - start_date).days . >>> DAYS(\"3/15/11\",\"2/1/11\") 42 >>> DAYS(DATE(2011, 12, 31), DATE(2011, 1, 1)) 364 >>> DAYS(\"2/1/11\", \"3/15/11\") -42 DTIME # DTIME (value, tz=None) # Returns the value converted to a python datetime object. The value may be a string , date (interpreted as midnight on that day), time (interpreted as a time-of-day today), or an existing datetime . The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. If the input is itself a datetime with the timezone set, it is returned unchanged (no changes to its timezone). >>> DTIME(datetime.date(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.date(2017, 1, 1), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('Europe/Paris')) >>> DTIME(datetime.datetime(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC'))) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC')), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) EDATE # EDATE (start_date, months) # Returns the date that is the given number of months before or after start_date . Use EDATE to calculate maturity dates or due dates that fall on the same day of the month as the date of issue. >>> EDATE(DATE(2011, 1, 15), 1) datetime.date(2011, 2, 15) >>> EDATE(DATE(2011, 1, 15), -1) datetime.date(2010, 12, 15) >>> EDATE(DATE(2011, 1, 15), 2) datetime.date(2011, 3, 15) >>> EDATE(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 1) >>> EDATE(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 1) EOMONTH # EOMONTH (start_date, months) # Returns the date for the last day of the month that is the indicated number of months before or after start_date. Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. >>> EOMONTH(DATE(2011, 1, 1), 1) datetime.date(2011, 2, 28) >>> EOMONTH(DATE(2011, 1, 15), -3) datetime.date(2010, 10, 31) >>> EOMONTH(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 31) >>> EOMONTH(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 31) HOUR # HOUR (time) # Same as time.hour . >>> HOUR(XL_TO_DATE(0.75)) 18 >>> HOUR(\"7/18/2011 7:45\") 7 >>> HOUR(\"4/21/2012\") 0 ISOWEEKNUM # ISOWEEKNUM (date) # Returns the ISO week number of the year for a given date. >>> ISOWEEKNUM(\"3/9/2012\") 10 >>> [ISOWEEKNUM(DATE(2000 + y, 1, 1)) for y in [0,1,2,3,4,5,6,7,8]] [52, 1, 1, 1, 1, 53, 52, 1, 1] MINUTE # MINUTE (time) # Returns the minutes of datetime , as an integer from 0 to 59. Same as time.minute . >>> MINUTE(XL_TO_DATE(0.75)) 0 >>> MINUTE(\"7/18/2011 7:45\") 45 >>> MINUTE(\"12:59:00 PM\") 59 >>> MINUTE(datetime.time(12, 58, 59)) 58 MONTH # MONTH (date) # Returns the month of a date represented, as an integer from from 1 (January) to 12 (December). Same as date.month . >>> MONTH(DATE(2011, 4, 15)) 4 >>> MONTH(\"5/31/2012\") 5 >>> MONTH(datetime.datetime(1900, 1, 1)) 1 NOW # NOW (tz=None) # Returns the datetime object for the current time. SECOND # SECOND (time) # Returns the seconds of datetime , as an integer from 0 to 59. Same as time.second . >>> SECOND(XL_TO_DATE(0.75)) 0 >>> SECOND(\"7/18/2011 7:45:13\") 13 >>> SECOND(datetime.time(12, 58, 59)) 59 TODAY # TODAY (tz=None) # Returns the date object for the current date. WEEKDAY # WEEKDAY (date, return_type=1) # Returns the day of the week corresponding to a date. The day is given as an integer, ranging from 1 (Sunday) to 7 (Saturday), by default. Return_type determines the type of the returned value. 1 (default) - Returns 1 (Sunday) through 7 (Saturday). 2 - Returns 1 (Monday) through 7 (Sunday). 3 - Returns 0 (Monday) through 6 (Sunday). 11 - Returns 1 (Monday) through 7 (Sunday). 12 - Returns 1 (Tuesday) through 7 (Monday). 13 - Returns 1 (Wednesday) through 7 (Tuesday). 14 - Returns 1 (Thursday) through 7 (Wednesday). 15 - Returns 1 (Friday) through 7 (Thursday). 16 - Returns 1 (Saturday) through 7 (Friday). 17 - Returns 1 (Sunday) through 7 (Saturday). >>> WEEKDAY(DATE(2008, 2, 14)) 5 >>> WEEKDAY(DATE(2012, 3, 1)) 5 >>> WEEKDAY(DATE(2012, 3, 1), 1) 5 >>> WEEKDAY(DATE(2012, 3, 1), 2) 4 >>> WEEKDAY(\"3/1/2012\", 3) 3 WEEKNUM # WEEKNUM (date, return_type=1) # Returns the week number of a specific date. For example, the week containing January 1 is the first week of the year, and is numbered week 1. Return_type determines which week is considered the first week of the year. 1 (default) - Week 1 is the first week starting Sunday that contains January 1. 2 - Week 1 is the first week starting Monday that contains January 1. 11 - Week 1 is the first week starting Monday that contains January 1. 12 - Week 1 is the first week starting Tuesday that contains January 1. 13 - Week 1 is the first week starting Wednesday that contains January 1. 14 - Week 1 is the first week starting Thursday that contains January 1. 15 - Week 1 is the first week starting Friday that contains January 1. 16 - Week 1 is the first week starting Saturday that contains January 1. 17 - Week 1 is the first week starting Sunday that contains January 1. 21 - ISO 8601 Approach: Week 1 is the first week starting Monday that contains January 4. Equivalently, it is the week that contains the first Thursday of the year. >>> WEEKNUM(DATE(2012, 3, 9)) 10 >>> WEEKNUM(DATE(2012, 3, 9), 2) 11 >>> WEEKNUM('1/1/1900') 1 >>> WEEKNUM('2/1/1900') 5 XL_TO_DATE # XL_TO_DATE (value, tz=None) # Converts a provided Excel serial number representing a date into a datetime object. Value is interpreted as the number of days since December 30, 1899. (This corresponds to Google Sheets interpretation. Excel starts with Dec. 31, 1899 but wrongly considers 1900 to be a leap year. Excel for Mac should be configured to use 1900 date system, i.e. uncheck \u201cUse the 1904 date system\u201d option.) The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. >>> XL_TO_DATE(41100.1875) datetime.datetime(2012, 7, 10, 4, 30, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(39448) datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(40982.0625) datetime.datetime(2012, 3, 14, 1, 30, tzinfo=moment.tzinfo('America/New_York')) YEAR # YEAR (date) # Returns the year corresponding to a date as an integer. Same as date.year . >>> YEAR(DATE(2011, 4, 15)) 2011 >>> YEAR(\"5/31/2030\") 2030 >>> YEAR(datetime.datetime(1900, 1, 1)) 1900 YEARFRAC # YEARFRAC (start_date, end_date, basis=0) # Calculates the fraction of the year represented by the number of whole days between two dates. Basis is the type of day count basis to use. 0 (default) - US (NASD) 30/360 1 - Actual/actual 2 - Actual/360 3 - Actual/365 4 - European 30/360 -1 - Actual/actual (Google Sheets variation) This function is useful for financial calculations. For compatibility with Excel, it defaults to using the NASD standard calendar. For use in non-financial settings, option -1 is likely the best choice. See https://en.wikipedia.org/wiki/360-day_calendar for explanation of the US 30/360 and European 30/360 methods. See http://www.dwheeler.com/yearfrac/ for analysis of Excel\u2019s particular implementation. Basis -1 is similar to 1 , but differs from Excel when dates span both leap and non-leap years. It matches the calculation in Google Sheets, counting the days in each year as a fraction of that year\u2019s length. Fraction of the year between 1/1/2012 and 7/30/12, omitting the Basis argument. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30)) '0.58055556' Fraction between same dates, using the Actual/Actual basis argument. Because 2012 is a Leap year, it has a 366 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 1) '0.57650273' Fraction between same dates, using the Actual/365 basis argument. Uses a 365 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 3) '0.57808219' Info # CELL # CELL (info_type, reference) # Returns the requested information about the specified cell. This is not implemented in Grist Note This function is not currently implemented in Grist. CURRENT_CONVERSION # CURRENT_CONVERSION (rec) # Internal function used by Grist during column type conversions. Not available for use in formulas. ISBLANK # ISBLANK (value) # Returns whether a value refers to an empty cell. It isn\u2019t implemented in Grist. To check for an empty string, use value == \"\" . Note This function is not currently implemented in Grist. ISEMAIL # ISEMAIL (value) # Returns whether a value is a valid email address. Note that checking email validity is not an exact science. The technical standard considers many email addresses valid that are not used in practice, and would not be considered valid by most users. Instead, we follow Google Sheets implementation, with some differences, noted below. >>> ISEMAIL(\"Abc.123@example.com\") True >>> ISEMAIL(\"Bob_O-Reilly+tag@example.com\") True >>> ISEMAIL(\"John Doe\") False >>> ISEMAIL(\"john@aol...com\") False ISERR # ISERR (value) # Checks whether a value is an error. In other words, it returns true if using value directly would raise an exception. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. A more Pythonic approach to checking for errors is: try: ... value ... except Exception, err: ... do something about the error ... For example: >>> ISERR(\"Hello\") False ISERROR # ISERROR (value) # Checks whether a value is an error or an invalid value. It is similar to ISERR , but also returns true for an invalid value such as NaN or a text value in a Numeric column. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. >>> ISERROR(\"Hello\") False >>> ISERROR(AltText(\"fail\")) True >>> ISERROR(float('nan')) True ISLOGICAL # ISLOGICAL (value) # Checks whether a value is True or False . >>> ISLOGICAL(True) True >>> ISLOGICAL(False) True >>> ISLOGICAL(0) False >>> ISLOGICAL(None) False >>> ISLOGICAL(\"Test\") False ISNA # ISNA (value) # Checks whether a value is the error #N/A . >>> ISNA(float('nan')) True >>> ISNA(0.0) False >>> ISNA('text') False >>> ISNA(float('-inf')) False ISNONTEXT # ISNONTEXT (value) # Checks whether a value is non-textual. >>> ISNONTEXT(\"asdf\") False >>> ISNONTEXT(\"\") False >>> ISNONTEXT(AltText(\"text\")) False >>> ISNONTEXT(17.0) True >>> ISNONTEXT(None) True >>> ISNONTEXT(datetime.date(2011, 1, 1)) True ISNUMBER # ISNUMBER (value) # Checks whether a value is a number. >>> ISNUMBER(17) True >>> ISNUMBER(-123.123423) True >>> ISNUMBER(False) True >>> ISNUMBER(float('nan')) True >>> ISNUMBER(float('inf')) True >>> ISNUMBER('17') False >>> ISNUMBER(None) False >>> ISNUMBER(datetime.date(2011, 1, 1)) False ISREF # ISREF (value) # Checks whether a value is a table record. For example, if a column person is of type Reference to the People table, then ISREF($person) is True . Similarly, ISREF(People.lookupOne(name=$name)) is True . For any other type of value, ISREF() would evaluate to False . >>> ISREF(17) False >>> ISREF(\"Roger\") False ISREFLIST # ISREFLIST (value) # Checks whether a value is a RecordSet , the type of values in Reference List columns. For example, if a column people is of type Reference List to the People table, then ISREFLIST($people) is True . Similarly, ISREFLIST(People.lookupRecords(name=$name)) is True . For any other type of value, ISREFLIST() would evaluate to False . >>> ISREFLIST(17) False >>> ISREFLIST(\"Roger\") False ISTEXT # ISTEXT (value) # Checks whether a value is text. >>> ISTEXT(\"asdf\") True >>> ISTEXT(\"\") True >>> ISTEXT(AltText(\"text\")) True >>> ISTEXT(17.0) False >>> ISTEXT(None) False >>> ISTEXT(datetime.date(2011, 1, 1)) False ISURL # ISURL (value) # Checks whether a value is a valid URL. It does not need to be fully qualified, or to include \u201chttp://\u201d and \u201cwww\u201d. It does not follow a standard, but attempts to work similarly to ISURL in Google Sheets, and to return True for text that is likely a URL. Valid protocols include ftp, http, https, gopher, mailto, news, telnet, and aim. >>> ISURL(\"http://www.getgrist.com\") True >>> ISURL(\"https://foo.com/test_(wikipedia)#cite-1\") True >>> ISURL(\"mailto://user@example.com\") True >>> ISURL(\"http:///a\") False N # N (value) # Returns the value converted to a number. True/False are converted to 1/0. A date is converted to Excel-style serial number of the date. Anything else is converted to 0. >>> N(7) 7 >>> N(7.1) 7.1 >>> N(\"Even\") 0 >>> N(\"7\") 0 >>> N(True) 1 >>> N(datetime.datetime(2011, 4, 17)) 40650.0 NA # NA () # Returns the \u201cvalue not available\u201d error, #N/A . >>> math.isnan(NA()) True PEEK # PEEK (func) # Evaluates the given expression without creating dependencies or requiring that referenced values are up to date, using whatever value it finds in a cell. This is useful for preventing circular reference errors, particularly in trigger formulas. For example, if the formula for A depends on $B and the formula for B depends on $A , then normally this would raise a circular reference error because each value needs to be calculated before the other. But if A uses PEEK($B) then it will simply get the value already stored in $B without requiring that $B is first calculated to the latest value. Therefore A will be calculated first, and B can use $A without problems. RECORD # RECORD (record_or_list, dates_as_iso=False, expand_refs=0) # Returns a Python dictionary with all fields in the given record. If a list of records is given, returns a list of corresponding Python dictionaries. If dates_as_iso is set, Date and DateTime values are converted to string using ISO 8601 format. If expand_refs is set to 1 or higher, Reference values are replaced with a RECORD representation of the referenced record, expanding the given number of levels. Error values present in cells of the record are replaced with None value, and a special key of \u201c error \u201d gets added containing the error messages for those cells. For example: {\"Ratio\": None, \"_error_\": {\"Ratio\": \"ZeroDivisionError: integer division or modulo by zero\"}} Note that care is needed to avoid circular references when using RECORD(), since it creates a dependency on every cell in the record. In case of RECORD(rec), the cell containing this call will be omitted from the resulting dictionary. For example: RECORD($Person) RECORD(rec) RECORD(People.lookupOne(First_Name=\"Alice\")) RECORD(People.lookupRecords(Department=\"HR\")) REQUEST # REQUEST (url, params=None, headers=None) # Note This function is not currently implemented in Grist. TYPE # TYPE (value) # Returns a number associated with the type of data passed into the function. This is not implemented in Grist. Use isinstance(value, type) or type(value) . Note This function is not currently implemented in Grist. Logical # AND # AND (logical_expression, *logical_expressions) # Returns True if all of the arguments are logically true, and False if any are false. Same as all([value1, value2, ...]) . >>> AND(1) True >>> AND(0) False >>> AND(1, 1) True >>> AND(1,2,3,4) True >>> AND(1,2,3,4,0) False FALSE # FALSE () # Returns the logical value False . You may also use the value False directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> FALSE() False IF # IF (logical_expression, value_if_true, value_if_false) # Returns one value if a logical expression is True and another if it is False . The equivalent Python expression is: value_if_true if logical_expression else value_if_false Since Grist supports multi-line formulas, you may also use Python blocks such as: if logical_expression: return value_if_true else: return value_if_false NOTE: Grist follows Excel model by only evaluating one of the value expressions, by automatically wrapping the expressions to use lazy evaluation. This allows IF(False, 1/0, 1) to evaluate to 1 rather than raise an exception. >>> IF(12, \"Yes\", \"No\") 'Yes' >>> IF(None, \"Yes\", \"No\") 'No' >>> IF(True, 0.85, 0.0) 0.85 >>> IF(False, 0.85, 0.0) 0.0 IFERROR # IFERROR (value, value_if_error=\u2019\u2018) # Returns the first argument if it is not an error value, otherwise returns the second argument if present, or a blank if the second argument is absent. NOTE: Grist handles values that raise an exception by wrapping them to use lazy evaluation. >>> IFERROR(float('nan'), \"**NAN**\") '**NAN**' >>> IFERROR(17.17, \"**NAN**\") 17.17 >>> IFERROR(\"Text\") 'Text' >>> IFERROR(AltText(\"hello\")) '' NOT # NOT (logical_expression) # True . Same as not logical_expression . >>> NOT(123) False >>> NOT(0) True OR # OR (logical_expression, *logical_expressions) # Returns True if any of the arguments is logically true, and false if all of the arguments are false. Same as any([value1, value2, ...]) . >>> OR(1) True >>> OR(0) False >>> OR(1, 1) True >>> OR(0, 1) True >>> OR(0, 0) False >>> OR(0,False,0.0,\"\",None) False >>> OR(0,None,3,0) True TRUE # TRUE () # Returns the logical value True . You may also use the value True directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> TRUE() True Lookup # lookupOne # UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email) lookupRecords # UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. ADDRESS # ADDRESS (row, column, absolute_relative_mode, use_a1_notation, sheet) # Returns a cell reference as a string. Note This function is not currently implemented in Grist. CHOOSE # CHOOSE (index, choice1, choice2) # Returns an element from a list of choices based on index. Note This function is not currently implemented in Grist. COLUMN # COLUMN (cell_reference=None) # Returns the column number of a specified cell, with A=1 . Note This function is not currently implemented in Grist. COLUMNS # COLUMNS (range) # Returns the number of columns in a specified array or range. Note This function is not currently implemented in Grist. CONTAINS # CONTAINS (value, match_empty=no_match_empty) # Use this marker with UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. For example: MoviesTable.lookupRecords(genre=CONTAINS(\"Drama\")) will return records in MoviesTable where the column genre is a list or other container such as [\"Comedy\", \"Drama\"] , i.e. \"Drama\" in $genre . Note that the column being looked up (e.g. genre ) must have values of a container type such as list, tuple, or set. In particular the values mustn\u2019t be strings, e.g. \"Comedy-Drama\" won\u2019t match even though \"Drama\" in \"Comedy-Drama\" is True in Python. It also won\u2019t match substrings within container elements, e.g. [\"Comedy-Drama\"] . You can optionally pass a second argument match_empty to indicate a value that should be matched against empty lists in the looked up column. For example, given this formula: MoviesTable.lookupRecords(genre=CONTAINS(g, match_empty='')) If g is '' (i.e. equal to match_empty ) then the column genre in the returned records will either be an empty list (or other container) or a list containing g as usual. GETPIVOTDATA # GETPIVOTDATA (value_name, any_pivot_table_cell, original_column_1, pivot_item_1=None, *args) # Extracts an aggregated value from a pivot table that corresponds to the specified row and column headings. Note This function is not currently implemented in Grist. HLOOKUP # HLOOKUP (search_key, range, index, is_sorted) # Horizontal lookup. Searches across the first row of a range for a key and returns the value of a specified cell in the column found. Note This function is not currently implemented in Grist. HYPERLINK # HYPERLINK (url, link_label) # Creates a hyperlink inside a cell. Note This function is not currently implemented in Grist. INDEX # INDEX (reference, row, column) # Returns the content of a cell, specified by row and column offset. Note This function is not currently implemented in Grist. INDIRECT # INDIRECT (cell_reference_as_string) # Returns a cell reference specified by a string. Note This function is not currently implemented in Grist. LOOKUP # LOOKUP (search_key, search_range_or_search_result_array, result_range=None) # Looks through a row or column for a key and returns the value of the cell in a result range located in the same position as the search row or column. Note This function is not currently implemented in Grist. MATCH # MATCH (search_key, range, search_type) # Returns the relative position of an item in a range that matches a specified value. Note This function is not currently implemented in Grist. OFFSET # OFFSET (cell_reference, offset_rows, offset_columns, height, width) # Returns a range reference shifted a specified number of rows and columns from a starting cell reference. Note This function is not currently implemented in Grist. ROW # ROW (cell_reference) # Returns the row number of a specified cell. Note This function is not currently implemented in Grist. ROWS # ROWS (range) # Returns the number of rows in a specified array or range. Note This function is not currently implemented in Grist. SELF_HYPERLINK # SELF_HYPERLINK (label=None, page=None, **kwargs) # Creates a link to the current document. All parameters are optional. The returned string is in URL format, optionally preceded by a label and a space (the format expected for Grist Text columns with the HyperLink option enabled). A numeric page number can be supplied, which will create a link to the specified page. To find the numeric page number you need, visit a page and examine its URL for a /p/NN part. Any number of arguments of the form LinkKey_NAME may be provided, to set user.LinkKey.NAME values that will be available in access rules. For example, if a rule allows users to view rows when user.LinkKey.Code == rec.Code , we might want to create links with SELF_HYPERLINK(LinkKey_Code=$Code) . >>> SELF_HYPERLINK() u'https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(label='doc') u'doc https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(page=2) u'https://docs.getgrist.com/sbaltsirg/Example/p/2' >>> SELF_HYPERLINK(LinkKey_Code='X1234') u'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234' >>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo') u'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo' >>> SELF_HYPERLINK(Linky_Link='Link') Traceback (most recent call last): ... TypeError: unexpected keyword argument 'Linky_Link' (not of form LinkKey_NAME) VLOOKUP # VLOOKUP (table, **field_value_pairs) # Vertical lookup. Searches the given table for a record matching the given field=value arguments. If multiple records match, returns one of them. If none match, returns the special empty record. The returned object is a record whose fields are available using .field syntax. For example, VLOOKUP(Employees, EmployeeID=$EmpID).Salary . Note that VLOOKUP isn\u2019t commonly needed in Grist, since Reference columns are the best way to link data between tables, and allow simple efficient usage such as $Person.Age . VLOOKUP is exactly quivalent to table.lookupOne(**field_value_pairs) . See lookupOne . For example: VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\") VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\").Age Math # ABS # ABS (value) # Returns the absolute value of a number. >>> ABS(2) 2 >>> ABS(-2) 2 >>> ABS(-4) 4 ACOS # ACOS (value) # Returns the inverse cosine of a value, in radians. >>> round(ACOS(-0.5), 9) 2.094395102 >>> round(ACOS(-0.5)*180/PI(), 10) 120.0 ACOSH # ACOSH (value) # Returns the inverse hyperbolic cosine of a number. >>> ACOSH(1) 0.0 >>> round(ACOSH(10), 7) 2.9932228 ARABIC # ARABIC (roman_numeral) # Computes the value of a Roman numeral. >>> ARABIC(\"LVII\") 57 >>> ARABIC('mcmxii') 1912 ASIN # ASIN (value) # Returns the inverse sine of a value, in radians. >>> round(ASIN(-0.5), 9) -0.523598776 >>> round(ASIN(-0.5)*180/PI(), 10) -30.0 >>> round(DEGREES(ASIN(-0.5)), 10) -30.0 ASINH # ASINH (value) # Returns the inverse hyperbolic sine of a number. >>> round(ASINH(-2.5), 9) -1.647231146 >>> round(ASINH(10), 9) 2.99822295 ATAN # ATAN (value) # Returns the inverse tangent of a value, in radians. >>> round(ATAN(1), 9) 0.785398163 >>> ATAN(1)*180/PI() 45.0 >>> DEGREES(ATAN(1)) 45.0 ATAN2 # ATAN2 (x, y) # Returns the angle between the x-axis and a line segment from the origin (0,0) to specified coordinate pair ( x , y ), in radians. >>> round(ATAN2(1, 1), 9) 0.785398163 >>> round(ATAN2(-1, -1), 9) -2.35619449 >>> ATAN2(-1, -1)*180/PI() -135.0 >>> DEGREES(ATAN2(-1, -1)) -135.0 >>> round(ATAN2(1,2), 9) 1.107148718 ATANH # ATANH (value) # Returns the inverse hyperbolic tangent of a number. >>> round(ATANH(0.76159416), 9) 1.00000001 >>> round(ATANH(-0.1), 9) -0.100335348 CEILING # CEILING (value, factor=1) # Rounds a number up to the nearest multiple of factor, or the nearest integer if the factor is omitted or 1. >>> CEILING(2.5, 1) 3 >>> CEILING(-2.5, -2) -4 >>> CEILING(-2.5, 2) -2 >>> CEILING(1.5, 0.1) 1.5 >>> CEILING(0.234, 0.01) 0.24 COMBIN # COMBIN (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects. >>> COMBIN(8,2) 28 >>> COMBIN(4,2) 6 >>> COMBIN(10,7) 120 COS # COS (angle) # Returns the cosine of an angle provided in radians. >>> round(COS(1.047), 7) 0.5001711 >>> round(COS(60*PI()/180), 10) 0.5 >>> round(COS(RADIANS(60)), 10) 0.5 COSH # COSH (value) # Returns the hyperbolic cosine of any real number. >>> round(COSH(4), 6) 27.308233 >>> round(COSH(EXP(1)), 7) 7.6101251 DEGREES # DEGREES (angle) # Converts an angle value in radians to degrees. >>> round(DEGREES(ACOS(-0.5)), 10) 120.0 >>> DEGREES(PI()) 180.0 EVEN # EVEN (value) # Rounds a number up to the nearest even integer, rounding away from zero. >>> EVEN(1.5) 2 >>> EVEN(3) 4 >>> EVEN(2) 2 >>> EVEN(-1) -2 EXP # EXP (exponent) # Returns Euler\u2019s number, e (~2.718) raised to a power. >>> round(EXP(1), 8) 2.71828183 >>> round(EXP(2), 7) 7.3890561 FACT # FACT (value) # Returns the factorial of a number. >>> FACT(5) 120 >>> FACT(1.9) 1 >>> FACT(0) 1 >>> FACT(1) 1 >>> FACT(-1) Traceback (most recent call last): ... ValueError: factorial() not defined for negative values FACTDOUBLE # FACTDOUBLE (value) # Returns the \u201cdouble factorial\u201d of a number. >>> FACTDOUBLE(6) 48 >>> FACTDOUBLE(7) 105 >>> FACTDOUBLE(3) 3 >>> FACTDOUBLE(4) 8 FLOOR # FLOOR (value, factor=1) # Rounds a number down to the nearest integer multiple of specified significance. >>> FLOOR(3.7,2) 2 >>> FLOOR(-2.5,-2) -2 >>> FLOOR(2.5,-2) Traceback (most recent call last): ... ValueError: factor argument invalid >>> FLOOR(1.58,0.1) 1.5 >>> FLOOR(0.234,0.01) 0.23 GCD # GCD (value1, *more_values) # Returns the greatest common divisor of one or more integers. >>> GCD(5, 2) 1 >>> GCD(24, 36) 12 >>> GCD(7, 1) 1 >>> GCD(5, 0) 5 >>> GCD(0, 5) 5 >>> GCD(5) 5 >>> GCD(14, 42, 21) 7 INT # INT (value) # Rounds a number down to the nearest integer that is less than or equal to it. >>> INT(8.9) 8 >>> INT(-8.9) -9 >>> 19.5-INT(19.5) 0.5 LCM # LCM (value1, *more_values) # Returns the least common multiple of one or more integers. >>> LCM(5, 2) 10 >>> LCM(24, 36) 72 >>> LCM(0, 5) 0 >>> LCM(5) 5 >>> LCM(10, 100) 100 >>> LCM(12, 18) 36 >>> LCM(12, 18, 24) 72 LN # LN (value) # Returns the the logarithm of a number, base e (Euler\u2019s number). >>> round(LN(86), 7) 4.4543473 >>> round(LN(2.7182818), 7) 1.0 >>> round(LN(EXP(3)), 10) 3.0 LOG # LOG (value, base=10) # Returns the the logarithm of a number given a base. >>> LOG(10) 1.0 >>> LOG(8, 2) 3.0 >>> round(LOG(86, 2.7182818), 7) 4.4543473 LOG10 # LOG10 (value) # Returns the the logarithm of a number, base 10. >>> round(LOG10(86), 9) 1.934498451 >>> LOG10(10) 1.0 >>> LOG10(100000) 5.0 >>> LOG10(10**5) 5.0 MOD # MOD (dividend, divisor) # Returns the result of the modulo operator, the remainder after a division operation. >>> MOD(3, 2) 1 >>> MOD(-3, 2) 1 >>> MOD(3, -2) -1 >>> MOD(-3, -2) -1 MROUND # MROUND (value, factor) # Rounds one number to the nearest integer multiple of another. >>> MROUND(10, 3) 9 >>> MROUND(-10, -3) -9 >>> round(MROUND(1.3, 0.2), 10) 1.4 >>> MROUND(5, -2) Traceback (most recent call last): ... ValueError: factor argument invalid MULTINOMIAL # MULTINOMIAL (value1, *more_values) # Returns the factorial of the sum of values divided by the product of the values\u2019 factorials. >>> MULTINOMIAL(2, 3, 4) 1260 >>> MULTINOMIAL(3) 1 >>> MULTINOMIAL(1,2,3) 60 >>> MULTINOMIAL(0,2,4,6) 13860 ODD # ODD (value) # Rounds a number up to the nearest odd integer. >>> ODD(1.5) 3 >>> ODD(3) 3 >>> ODD(2) 3 >>> ODD(-1) -1 >>> ODD(-2) -3 PI # PI () # Returns the value of Pi to 14 decimal places. >>> round(PI(), 9) 3.141592654 >>> round(PI()/2, 9) 1.570796327 >>> round(PI()*9, 8) 28.27433388 POWER # POWER (base, exponent) # Returns a number raised to a power. >>> POWER(5,2) 25.0 >>> round(POWER(98.6,3.2), 3) 2401077.222 >>> round(POWER(4,5.0/4), 9) 5.656854249 PRODUCT # PRODUCT (factor1, *more_factors) # Returns the result of multiplying a series of numbers together. Each argument may be a number or an array. >>> PRODUCT([5,15,30]) 2250 >>> PRODUCT([5,15,30], 2) 4500 >>> PRODUCT(5,15,[30],[2]) 4500 QUOTIENT # QUOTIENT (dividend, divisor) # Returns one number divided by another, without the remainder. >>> QUOTIENT(5, 2) 2 >>> QUOTIENT(4.5, 3.1) 1 >>> QUOTIENT(-10, 3) -3 RADIANS # RADIANS (angle) # Converts an angle value in degrees to radians. >>> round(RADIANS(270), 6) 4.712389 RAND # RAND () # Returns a random number between 0 inclusive and 1 exclusive. RANDBETWEEN # RANDBETWEEN (low, high) # Returns a uniformly random integer between two values, inclusive. ROMAN # ROMAN (number, form_unused=None) # Formats a number in Roman numerals. The second argument is ignored in this implementation. >>> ROMAN(499,0) 'CDXCIX' >>> ROMAN(499.2,0) 'CDXCIX' >>> ROMAN(57) 'LVII' >>> ROMAN(1912) 'MCMXII' ROUND # ROUND (value, places=0) # Rounds a number to a certain number of decimal places, by default to the nearest whole number if the number of places is not given. Rounds away from zero (\u2018up\u2019 for positive numbers) in the case of a tie, i.e. when the last digit is 5. >>> ROUND(1.4) 1.0 >>> ROUND(1.5) 2.0 >>> ROUND(2.5) 3.0 >>> ROUND(-2.5) -3.0 >>> ROUND(2.15, 1) 2.2 >>> ROUND(-1.475, 2) -1.48 >>> ROUND(21.5, -1) 20.0 >>> ROUND(626.3,-3) 1000.0 >>> ROUND(1.98,-1) 0.0 >>> ROUND(-50.55,-2) -100.0 >>> ROUND(0) 0.0 ROUNDDOWN # ROUNDDOWN (value, places=0) # Rounds a number to a certain number of decimal places, always rounding down towards zero. >>> ROUNDDOWN(3.2, 0) 3 >>> ROUNDDOWN(76.9,0) 76 >>> ROUNDDOWN(3.14159, 3) 3.141 >>> ROUNDDOWN(-3.14159, 1) -3.1 >>> ROUNDDOWN(31415.92654, -2) 31400 ROUNDUP # ROUNDUP (value, places=0) # Rounds a number to a certain number of decimal places, always rounding up away from zero. >>> ROUNDUP(3.2,0) 4 >>> ROUNDUP(76.9,0) 77 >>> ROUNDUP(3.14159, 3) 3.142 >>> ROUNDUP(-3.14159, 1) -3.2 >>> ROUNDUP(31415.92654, -2) 31500 SERIESSUM # SERIESSUM (x, n, m, a) # Given parameters x, n, m, and a, returns the power series sum a_1 x^n + a_2 x^(n+m) + \u2026 + a_i*x^(n+(i-1)m), where i is the number of entries in range a . >>> SERIESSUM(1,0,1,1) 1 >>> SERIESSUM(2,1,0,[1,2,3]) 12 >>> SERIESSUM(-3,1,1,[2,4,6]) -132 >>> round(SERIESSUM(PI()/4,0,2,[1,-1./FACT(2),1./FACT(4),-1./FACT(6)]), 6) 0.707103 SIGN # SIGN (value) # Given an input number, returns -1 if it is negative, 1 if positive, and 0 if it is zero. >>> SIGN(10) 1 >>> SIGN(4.0-4.0) 0 >>> SIGN(-0.00001) -1 SIN # SIN (angle) # Returns the sine of an angle provided in radians. >>> round(SIN(PI()), 10) 0.0 >>> SIN(PI()/2) 1.0 >>> round(SIN(30*PI()/180), 10) 0.5 >>> round(SIN(RADIANS(30)), 10) 0.5 SINH # SINH (value) # Returns the hyperbolic sine of any real number. >>> round(2.868*SINH(0.0342*1.03), 7) 0.1010491 SQRT # SQRT (value) # Returns the positive square root of a positive number. >>> SQRT(16) 4.0 >>> SQRT(-16) Traceback (most recent call last): ... ValueError: math domain error >>> SQRT(ABS(-16)) 4.0 SQRTPI # SQRTPI (value) # Returns the positive square root of the product of Pi and the given positive number. >>> round(SQRTPI(1), 6) 1.772454 >>> round(SQRTPI(2), 6) 2.506628 SUBTOTAL # SUBTOTAL (function_code, range1, range2) # Returns a subtotal for a vertical range of cells using a specified aggregation function. Note This function is not currently implemented in Grist. SUM # SUM (value1, *more_values) # Returns the sum of a series of numbers. Each argument may be a number or an array. Non-numeric values are ignored. >>> SUM([5,15,30]) 50 >>> SUM([5.,15,30], 2) 52.0 >>> SUM(5,15,[30],[2]) 52 SUMIF # SUMIF (records, criterion, sum_range) # Returns a conditional sum across a range. Note This function is not currently implemented in Grist. SUMIFS # SUMIFS (sum_range, criteria_range1, criterion1, *args) # Returns the sum of a range depending on multiple criteria. Note This function is not currently implemented in Grist. SUMPRODUCT # SUMPRODUCT (array1, *more_arrays) # Multiplies corresponding components in two equally-sized arrays, and returns the sum of those products. >>> SUMPRODUCT([3,8,1,4,6,9], [2,6,5,7,7,3]) 156 >>> SUMPRODUCT([], [], []) 0 >>> SUMPRODUCT([-0.25], [-2], [-3]) -1.5 >>> SUMPRODUCT([-0.25, -0.25], [-2, -2], [-3, -3]) -3.0 SUMSQ # SUMSQ (value1, value2) # Returns the sum of the squares of a series of numbers and/or cells. Note This function is not currently implemented in Grist. TAN # TAN (angle) # Returns the tangent of an angle provided in radians. >>> round(TAN(0.785), 8) 0.99920399 >>> round(TAN(45*PI()/180), 10) 1.0 >>> round(TAN(RADIANS(45)), 10) 1.0 TANH # TANH (value) # Returns the hyperbolic tangent of any real number. >>> round(TANH(-2), 6) -0.964028 >>> TANH(0) 0.0 >>> round(TANH(0.5), 6) 0.462117 TRUNC # TRUNC (value, places=0) # Truncates a number to a certain number of significant digits by omitting less significant digits. >>> TRUNC(8.9) 8 >>> TRUNC(-8.9) -8 >>> TRUNC(0.45) 0 UUID # UUID () # Generate a random UUID-formatted string identifier. Since UUID() produces a different value each time it\u2019s called, it is best to use it in trigger formula for new records. This would only calculate UUID() once and freeze the calculated value. By contrast, a regular formula may get recalculated any time the document is reloaded, producing a different value for UUID() each time. Schedule # SCHEDULE # SCHEDULE (schedule, start=None, count=10, end=None) # Returns the list of datetime objects generated according to the schedule string. Starts at start , which defaults to NOW(). Generates at most count results (10 by default). If end is given, stops there. The schedule has the format \u201cINTERVAL: SLOTS, \u2026\u201d. For example: annual: Jan-15, Apr-15, Jul-15 -- Three times a year on given dates at midnight. annual: 1/15, 4/15, 7/15 -- Same as above. monthly: /1 2pm, /15 2pm -- The 1st and the 15th of each month, at 2pm. 3-months: /10, +1m /20 -- Every 3 months on the 10th of month 1, 20th of month 2. weekly: Mo 9am, Tu 9am, Fr 2pm -- Three times a week at specified times. 2-weeks: Mo, +1w Tu -- Every 2 weeks on Monday of week 1, Tuesday of week 2. daily: 07:30, 21:00 -- Twice a day at specified times. 2-day: 12am, 4pm, +1d 8am -- Three times every two days, evenly spaced. hourly: :15, :45 -- 15 minutes before and after each hour. 4-hour: :00, 1:20, 2:40 -- Three times every 4 hours, evenly spaced. 10-minute: +0s -- Every 10 minutes on the minute. INTERVAL must be either of the form N-unit where N is a number and unit is one of year , month , week , day , hour ; or one of the aliases: annual , monthly , weekly , daily , hourly , which mean 1-year , 1-month , etc. SLOTS support the following units: `Jan-15` or `1/15` -- Month and day of the month; available when INTERVAL is year-based. `/15` -- Day of the month, available when INTERVAL is month-based. `Mon`, `Mo`, `Friday` -- Day of the week (or abbreviation), when INTERVAL is week-based. 10am, 1:30pm, 15:45 -- Time of day, available for day-based or longer intervals. :45, :00 -- Minutes of the hour, available when INTERVAL is hour-based. +1d, +15d -- How many days to add to start of INTERVAL. +1w -- How many weeks to add to start of INTERVAL. +1m -- How many months to add to start of INTERVAL. The SLOTS are always relative to the INTERVAL rather than to start . Week-based intervals start on Sunday. E.g. weekly: +1d, +4d is the same as weekly: Mon, Thu , and generates times on Mondays and Thursdays regardless of start . The first generated time is determined by the unit of the INTERVAL without regard to the multiple. E.g. both \u201c2-week: Mon\u201d and \u201c3-week: Mon\u201d start on the first Monday after start , and then generate either every second or every third Monday after that. Similarly, 24-hour: :00 starts with the first top-of-the-hour after start (not with midnight), and then repeats every 24 hours. To start with the midnight after start , use daily: 0:00 . For interval units of a day or longer, if time-of-day is not specified, it defaults to midnight. The time zone of start determines the time zone of the generated times. >>> def show(dates): return [d.strftime(\"%Y-%m-%d %H:%M\") for d in dates] >>> start = datetime(2018, 9, 4, 14, 0); # 2pm on Tue, Sep 4 2018. >>> show(SCHEDULE('annual: Jan-15, Apr-15, Jul-15, Oct-15', start=start, count=4)) ['2018-10-15 00:00', '2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00'] >>> show(SCHEDULE('annual: 1/15, 4/15, 7/15', start=start, count=4)) ['2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00', '2020-01-15 00:00'] >>> show(SCHEDULE('monthly: /1 2pm, /15 5pm', start=start, count=4)) ['2018-09-15 17:00', '2018-10-01 14:00', '2018-10-15 17:00', '2018-11-01 14:00'] >>> show(SCHEDULE('3-months: /10, +1m /20', start=start, count=4)) ['2018-09-10 00:00', '2018-10-20 00:00', '2018-12-10 00:00', '2019-01-20 00:00'] >>> show(SCHEDULE('weekly: Mo 9am, Tu 9am, Fr 2pm', start=start, count=4)) ['2018-09-07 14:00', '2018-09-10 09:00', '2018-09-11 09:00', '2018-09-14 14:00'] >>> show(SCHEDULE('2-weeks: Mo, +1w Tu', start=start, count=4)) ['2018-09-11 00:00', '2018-09-17 00:00', '2018-09-25 00:00', '2018-10-01 00:00'] >>> show(SCHEDULE('daily: 07:30, 21:00', start=start, count=4)) ['2018-09-04 21:00', '2018-09-05 07:30', '2018-09-05 21:00', '2018-09-06 07:30'] >>> show(SCHEDULE('2-day: 12am, 4pm, +1d 8am', start=start, count=4)) ['2018-09-04 16:00', '2018-09-05 08:00', '2018-09-06 00:00', '2018-09-06 16:00'] >>> show(SCHEDULE('hourly: :15, :45', start=start, count=4)) ['2018-09-04 14:15', '2018-09-04 14:45', '2018-09-04 15:15', '2018-09-04 15:45'] >>> show(SCHEDULE('4-hour: :00, +1H :20, +2H :40', start=start, count=4)) ['2018-09-04 14:00', '2018-09-04 15:20', '2018-09-04 16:40', '2018-09-04 18:00'] Stats # AVEDEV # AVEDEV (value1, value2) # Calculates the average of the magnitudes of deviations of data from a dataset\u2019s mean. Note This function is not currently implemented in Grist. AVERAGE # AVERAGE (value, *more_values) # Returns the numerical average value in a dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. >>> AVERAGE([2, -1.0, 11]) 4.0 >>> AVERAGE([2, -1, 11, \"Hello\"]) 4.0 >>> AVERAGE([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11]) 4.0 >>> AVERAGE(False, True) Traceback (most recent call last): ... ZeroDivisionError: float division by zero AVERAGEA # AVERAGEA (value, *more_values) # Returns the numerical average value in a dataset, counting non-numerical values as 0. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. >>> AVERAGEA([2, -1.0, 11]) 4.0 >>> AVERAGEA([2, -1, 11, \"Hello\"]) 3.0 >>> AVERAGEA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 1.5 >>> AVERAGEA(False, True) 0.5 AVERAGEIF # AVERAGEIF (criteria_range, criterion, average_range=None) # Returns the average of a range depending on criteria. Note This function is not currently implemented in Grist. AVERAGEIFS # AVERAGEIFS (average_range, criteria_range1, criterion1, *args) # Returns the average of a range depending on multiple criteria. Note This function is not currently implemented in Grist. AVERAGE_WEIGHTED # AVERAGE_WEIGHTED (pairs) # Given a list of (value, weight) pairs, finds the average of the values weighted by the corresponding weights. Ignores any pairs with a non-numerical value or weight. If you have two lists, of values and weights, use the Python built-in zip() function to create a list of pairs. >>> AVERAGE_WEIGHTED(((95, .25), (90, .1), (\"X\", .5), (85, .15), (88, .2), (82, .3), (70, None))) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, \"X\", 85, 88, 82, 70], [25, 10, 50, 15, 20, 30, None])) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, False, 85, 88, 82, 70], [.25, .1, .5, .15, .2, .3, True])) 87.7 BINOMDIST # BINOMDIST (num_successes, num_trials, prob_success, cumulative) # Calculates the probability of drawing a certain number of successes (or a maximum number of successes) in a certain number of tries given a population of a certain size containing a certain number of successes, with replacement of draws. Note This function is not currently implemented in Grist. CONFIDENCE # CONFIDENCE (alpha, standard_deviation, pop_size) # Calculates the width of half the confidence interval for a normal distribution. Note This function is not currently implemented in Grist. CORREL # CORREL (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist. COUNT # COUNT (value, *more_values) # Returns the count of numerical and date/datetime values in a dataset, ignoring other types of values. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. >>> COUNT([2, -1.0, 11]) 3 >>> COUNT([2, -1, 11, \"Hello\"]) 3 >>> COUNT([DATE(2000, 1, 1), DATE(2000, 1, 2), DATE(2000, 1, 3), \"Hello\"]) 3 >>> COUNT([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 4 >>> COUNT(False, True) 0 COUNTA # COUNTA (value, *more_values) # Returns the count of all values in a dataset, including non-numerical values. Each argument may be a value or an array. >>> COUNTA([2, -1.0, 11]) 3 >>> COUNTA([2, -1, 11, \"Hello\"]) 4 >>> COUNTA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 9 >>> COUNTA(False, True) 2 COVAR # COVAR (data_y, data_x) # Calculates the covariance of a dataset. Note This function is not currently implemented in Grist. CRITBINOM # CRITBINOM (num_trials, prob_success, target_prob) # Calculates the smallest value for which the cumulative binomial distribution is greater than or equal to a specified criteria. Note This function is not currently implemented in Grist. DEVSQ # DEVSQ (value1, value2) # Calculates the sum of squares of deviations based on a sample. Note This function is not currently implemented in Grist. EXPONDIST # EXPONDIST (x, lambda_, cumulative) # Returns the value of the exponential distribution function with a specified lambda at a specified value. Note This function is not currently implemented in Grist. FDIST # FDIST (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist. FISHER # FISHER (value) # Returns the Fisher transformation of a specified value. Note This function is not currently implemented in Grist. FISHERINV # FISHERINV (value) # Returns the inverse Fisher transformation of a specified value. Note This function is not currently implemented in Grist. FORECAST # FORECAST (x, data_y, data_x) # Calculates the expected y-value for a specified x based on a linear regression of a dataset. Note This function is not currently implemented in Grist. F_DIST # F_DIST (x, degrees_freedom1, degrees_freedom2, cumulative) # Calculates the left-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist. F_DIST_RT # F_DIST_RT (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist. GEOMEAN # GEOMEAN (value1, value2) # Calculates the geometric mean of a dataset. Note This function is not currently implemented in Grist. HARMEAN # HARMEAN (value1, value2) # Calculates the harmonic mean of a dataset. Note This function is not currently implemented in Grist. HYPGEOMDIST # HYPGEOMDIST (num_successes, num_draws, successes_in_pop, pop_size) # Calculates the probability of drawing a certain number of successes in a certain number of tries given a population of a certain size containing a certain number of successes, without replacement of draws. Note This function is not currently implemented in Grist. INTERCEPT # INTERCEPT (data_y, data_x) # Calculates the y-value at which the line resulting from linear regression of a dataset will intersect the y-axis (x=0). Note This function is not currently implemented in Grist. KURT # KURT (value1, value2) # Calculates the kurtosis of a dataset, which describes the shape, and in particular the \u201cpeakedness\u201d of that dataset. Note This function is not currently implemented in Grist. LARGE # LARGE (data, n) # Returns the nth largest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist. LOGINV # LOGINV (x, mean, standard_deviation) # Returns the value of the inverse log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist. LOGNORMDIST # LOGNORMDIST (x, mean, standard_deviation) # Returns the value of the log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist. MAX # MAX (value, *more_values) # Returns the maximum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MAX([2, -1.5, 11.5]) 11.5 >>> MAX([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAX(True, -123) -123 >>> MAX(\"123\", -123) -123 >>> MAX(\"Hello\", \"123\", True, False) 0 >>> MAX(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 2) >>> MAX(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56) >>> MAX(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 2) MAXA # MAXA (value, *more_values) # Returns the maximum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MAXA([2, -1.5, 11.5]) 11.5 >>> MAXA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAXA(True, -123) 1 >>> MAXA(\"123\", -123) 0 >>> MAXA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0 MEDIAN # MEDIAN (value, *more_values) # Returns the median value in a numeric dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. Produces an error if the arguments contain no numbers. The median is the middle number when all values are sorted. So half of the values in the dataset are less than the median, and half of the values are greater. If there is an even number of values in the dataset, returns the average of the two numbers in the middle. >>> MEDIAN(1, 2, 3, 4, 5) 3 >>> MEDIAN(3, 5, 1, 4, 2) 3 >>> MEDIAN(range(10)) 4.5 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1), 12.3) 12.3 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1)) Traceback (most recent call last): ... ValueError: MEDIAN requires at least one number MIN # MIN (value, *more_values) # Returns the minimum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MIN([2, -1.5, 11.5]) -1.5 >>> MIN([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MIN(True, 123) 123 >>> MIN(\"-123\", 123) 123 >>> MIN(\"Hello\", \"123\", True, False) 0 >>> MIN(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56) MINA # MINA (value, *more_values) # Returns the minimum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MINA([2, -1.5, 11.5]) -1.5 >>> MINA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MINA(True, 123) 1 >>> MINA(\"-123\", 123) 0 >>> MINA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0 MODE # MODE (value1, value2) # Returns the most commonly occurring value in a dataset. Note This function is not currently implemented in Grist. NEGBINOMDIST # NEGBINOMDIST (num_failures, num_successes, prob_success) # Calculates the probability of drawing a certain number of failures before a certain number of successes given a probability of success in independent trials. Note This function is not currently implemented in Grist. NORMDIST # NORMDIST (x, mean, standard_deviation, cumulative) # Returns the value of the normal distribution function (or normal cumulative distribution function) for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist. NORMINV # NORMINV (x, mean, standard_deviation) # Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist. NORMSDIST # NORMSDIST (x) # Returns the value of the standard normal cumulative distribution function for a specified value. Note This function is not currently implemented in Grist. NORMSINV # NORMSINV (x) # Returns the value of the inverse standard normal distribution function for a specified value. Note This function is not currently implemented in Grist. PEARSON # PEARSON (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist. PERCENTILE # PERCENTILE (data, percentile) # Returns the value at a given percentile of a dataset. Note This function is not currently implemented in Grist. PERCENTRANK # PERCENTRANK (data, value, significant_digits=None) # Returns the percentage rank (percentile) of a specified value in a dataset. Note This function is not currently implemented in Grist. PERCENTRANK_EXC # PERCENTRANK_EXC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 exclusive of a specified value in a dataset. Note This function is not currently implemented in Grist. PERCENTRANK_INC # PERCENTRANK_INC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 inclusive of a specified value in a dataset. Note This function is not currently implemented in Grist. PERMUT # PERMUT (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects, considering order. Note This function is not currently implemented in Grist. POISSON # POISSON (x, mean, cumulative) # Returns the value of the Poisson distribution function (or Poisson cumulative distribution function) for a specified value and mean. Note This function is not currently implemented in Grist. PROB # PROB (data, probabilities, low_limit, high_limit=None) # Given a set of values and corresponding probabilities, calculates the probability that a value chosen at random falls between two limits. Note This function is not currently implemented in Grist. QUARTILE # QUARTILE (data, quartile_number) # Returns a value nearest to a specified quartile of a dataset. Note This function is not currently implemented in Grist. RANK # RANK (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. Note This function is not currently implemented in Grist. RANK_AVG # RANK_AVG (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the average rank of the entries will be returned. Note This function is not currently implemented in Grist. RANK_EQ # RANK_EQ (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the top rank of the entries will be returned. Note This function is not currently implemented in Grist. RSQ # RSQ (data_y, data_x) # Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist. SKEW # SKEW (value1, value2) # Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean. Note This function is not currently implemented in Grist. SLOPE # SLOPE (data_y, data_x) # Calculates the slope of the line resulting from linear regression of a dataset. Note This function is not currently implemented in Grist. SMALL # SMALL (data, n) # Returns the nth smallest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist. STANDARDIZE # STANDARDIZE (value, mean, standard_deviation) # Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution. Note This function is not currently implemented in Grist. STDEV # STDEV (value, *more_values) # Calculates the standard deviation based on a sample, ignoring non-numerical values. >>> STDEV([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10], 3, 12, 15) 4.810702354423639 >>> STDEV([2, 5, 8, 13, 10], [3, 12, 15]) 4.810702354423639 >>> STDEV([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero STDEVA # STDEVA (value, *more_values) # Calculates the standard deviation based on a sample, setting text to the value 0 . >>> STDEVA([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEVA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], 1, 0, 0) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], [1, 0, 0]) 4.969550137731641 >>> STDEVA([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero STDEVP # STDEVP (value, *more_values) # Calculates the standard deviation based on an entire population, ignoring non-numerical values. >>> STDEVP([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10, True, False, \"Test\"]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10], 3, 12, 15) 4.5 >>> STDEVP([2, 5, 8, 13, 10], [3, 12, 15]) 4.5 >>> STDEVP([5]) 0.0 STDEVPA # STDEVPA (value, *more_values) # Calculates the standard deviation based on an entire population, setting text to the value 0 . >>> STDEVPA([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVPA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], 1, 0, 0) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], [1, 0, 0]) 4.648588495446763 >>> STDEVPA([5]) 0.0 STEYX # STEYX (data_y, data_x) # Calculates the standard error of the predicted y-value for each x in the regression of a dataset. Note This function is not currently implemented in Grist. TDIST # TDIST (x, degrees_freedom, tails) # Calculates the probability for Student\u2019s t-distribution with a given input (x). Note This function is not currently implemented in Grist. TINV # TINV (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist. TRIMMEAN # TRIMMEAN (data, exclude_proportion) # Calculates the mean of a dataset excluding some proportion of data from the high and low ends of the dataset. Note This function is not currently implemented in Grist. TTEST # TTEST (range1, range2, tails, type) # Returns the probability associated with t-test. Determines whether two samples are likely to have come from the same two underlying populations that have the same mean. Note This function is not currently implemented in Grist. T_INV # T_INV (probability, degrees_freedom) # Calculates the negative inverse of the one-tailed TDIST function. Note This function is not currently implemented in Grist. T_INV_2T # T_INV_2T (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist. VAR # VAR (value1, value2) # Calculates the variance based on a sample. Note This function is not currently implemented in Grist. VARA # VARA (value1, value2) # Calculates an estimate of variance based on a sample, setting text to the value 0 . Note This function is not currently implemented in Grist. VARP # VARP (value1, value2) # Calculates the variance based on an entire population. Note This function is not currently implemented in Grist. VARPA # VARPA (value1, value2) # Calculates the variance based on an entire population, setting text to the value 0 . Note This function is not currently implemented in Grist. WEIBULL # WEIBULL (x, shape, scale, cumulative) # Returns the value of the Weibull distribution function (or Weibull cumulative distribution function) for a specified shape and scale. Note This function is not currently implemented in Grist. ZTEST # ZTEST (data, value, standard_deviation) # Returns the two-tailed P-value of a Z-test with standard distribution. Note This function is not currently implemented in Grist. Text # CHAR # CHAR (table_number) # Convert a number into a character according to the current Unicode table. Same as unichr(number) . >>> CHAR(65) u'A' >>> CHAR(33) u'!' CLEAN # CLEAN (text) # Returns the text with the non-printable characters removed. This removes both characters with values 0 through 31, and other Unicode characters in the \u201ccontrol characters\u201d category. >>> CLEAN(CHAR(9) + \"Monthly report\" + CHAR(10)) u'Monthly report' CODE # CODE (string) # Returns the numeric Unicode map value of the first character in the string provided. Same as ord(string[0]) . >>> CODE(\"A\") 65 >>> CODE(\"!\") 33 >>> CODE(\"!A\") 33 CONCAT # CONCAT (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCATENATE . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCAT(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCAT(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCAT(\"abc\") u'abc' >>> CONCAT(0, \"abc\") u'0abc' >>> assert CONCAT(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' CONCATENATE # CONCATENATE (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCAT . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCATENATE(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCATENATE(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCATENATE(\"abc\") u'abc' >>> CONCATENATE(0, \"abc\") u'0abc' >>> assert CONCATENATE(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", \"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' DOLLAR # DOLLAR (number, decimals=2) # Formats a number into a formatted dollar amount, with decimals rounded to the specified place (. If decimals value is omitted, it defaults to 2. >>> DOLLAR(1234.567) '$1,234.57' >>> DOLLAR(1234.567, -2) '$1,200' >>> DOLLAR(-1234.567, -2) '($1,200)' >>> DOLLAR(-0.123, 4) '($0.1230)' >>> DOLLAR(99.888) '$99.89' >>> DOLLAR(0) '$0.00' >>> DOLLAR(10, 0) '$10' EXACT # EXACT (string1, string2) # Tests whether two strings are identical. Same as string2 == string2 . >>> EXACT(\"word\", \"word\") True >>> EXACT(\"Word\", \"word\") False >>> EXACT(\"w ord\", \"word\") False FIND # FIND (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> FIND(\"M\", \"Miriam McGovern\") 1 >>> FIND(\"m\", \"Miriam McGovern\") 6 >>> FIND(\"M\", \"Miriam McGovern\", 3) 8 >>> FIND(\" #\", \"Hello world # Test\") 12 >>> FIND(\"gle\", \"Google\", 1) 4 >>> FIND(\"GLE\", \"Google\", 1) Traceback (most recent call last): ... ValueError: substring not found >>> FIND(\"page\", \"homepage\") 5 >>> FIND(\"page\", \"homepage\", 6) Traceback (most recent call last): ... ValueError: substring not found FIXED # FIXED (number, decimals=2, no_commas=False) # Formats a number with a fixed number of decimal places (2 by default), and commas. If no_commas is True, then omits the commas. >>> FIXED(1234.567, 1) '1,234.6' >>> FIXED(1234.567, -1) '1,230' >>> FIXED(-1234.567, -1, True) '-1230' >>> FIXED(44.332) '44.33' >>> FIXED(3521.478, 2, False) '3,521.48' >>> FIXED(-3521.478, 1, True) '-3521.5' >>> FIXED(3521.478, 0, True) '3521' >>> FIXED(3521.478, -2, True) '3500' LEFT # LEFT (string, num_chars=1) # Returns a substring of length num_chars from the beginning of the given string. If num_chars is omitted, it is assumed to be 1. Same as string[:num_chars] . >>> LEFT(\"Sale Price\", 4) 'Sale' >>> LEFT('Swededn') 'S' >>> LEFT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid LEN # LEN (text) # Returns the number of characters in a text string, or the number of items in a list. Same as len in python. See Record Set for an example of using len on a list of records. >>> LEN(\"Phoenix, AZ\") 11 >>> LEN(\"\") 0 >>> LEN(\" One \") 11 LOWER # LOWER (text) # Converts a specified string to lowercase. Same as text.lower() . >>> LOWER(\"E. E. Cummings\") 'e. e. cummings' >>> LOWER(\"Apt. 2B\") 'apt. 2b' MID # MID (text, start_num, num_chars) # Returns a segment of a string, starting at start_num. The first character in text has start_num 1. >>> MID(\"Fluid Flow\", 1, 5) 'Fluid' >>> MID(\"Fluid Flow\", 7, 20) 'Flow' >>> MID(\"Fluid Flow\", 20, 5) '' >>> MID(\"Fluid Flow\", 0, 5) Traceback (most recent call last): ... ValueError: start_num invalid PHONE_FORMAT # PHONE_FORMAT (value, country=None, format=None) # Formats a phone number. With no optional arguments, the number must start with \u201c+\u201d and the international dialing prefix, and will be formatted as an international number, e.g. +12345678901 becomes +1 234-567-8901 . The country argument allows specifying a 2-letter country code (e.g. \u201cUS\u201d or \u201cGB\u201d) for interpreting phone numbers that don\u2019t start with \u201c+\u201d. E.g. PHONE_FORMAT('2025555555', 'US') would be seen as a US number and formatted as \u201c(202) 555-5555\u201d. Phone numbers that start with \u201c+\u201d ignore country . E.g. PHONE_FORMAT('+33555555555', 'US') is a French number because \u2018+33\u2019 is the international prefix for France. The format argument specifies the output format, according to this table: \"#\" or \"NATL\" (default) - use the national format, without the international dialing prefix, when possible. E.g. (234) 567-8901 for \u201cUS\u201d, or 02 34 56 78 90 for \u201cFR\u201d. If country is omitted, or the number does not correspond to the given country, the international format is used instead. \"+\" or \"INTL\" - international format, e.g. +1 234-567-8901 or +33 2 34 56 78 90 . \"*\" or \"E164\" - E164 format, like international but with no separators, e.g. +12345678901 . \"tel\" or \"RFC3966\" - format suitable to use as a hyperlink , e.g. \u2018tel:+1-234-567-8901\u2019. When specifying the format argument, you may omit the country argument. I.e. PHONE_FORMAT(value, \"tel\") is equivalent to PHONE_FORMAT(value, None, \"tel\") . For more details, see the phonenumbers Python library, which underlies this function. >>> PHONE_FORMAT(\"+12345678901\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"2345678901\", \"US\") u'(234) 567-8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\", \"+\") u'+44 23 4567 8901' >>> PHONE_FORMAT(\"+442345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"+12345678901\", \"GB\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"(234) 567-8901\") Traceback (most recent call last): ... NumberParseException: (0) Missing or invalid default region. >>> PHONE_FORMAT(\"(234)567 89-01\", \"US\", \"tel\") u'tel:+1-234-567-8901' >>> PHONE_FORMAT(\"2/3456/7890\", \"FR\", '#') u'02 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", '#') u'+33 2 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", 'tel') u'tel:+33-2-34-56-78-90' >>> PHONE_FORMAT(\"tel:+1-234-567-8901\", country=\"US\", format=\"*\") u'+12345678901' PROPER # PROPER (text) # Capitalizes each word in a specified string. It converts the first letter of each word to uppercase, and all other letters to lowercase. Same as text.title() . >>> PROPER('this is a TITLE') 'This Is A Title' >>> PROPER('2-way street') '2-Way Street' >>> PROPER('76BudGet') '76Budget' REGEXEXTRACT # REGEXEXTRACT (text, regular_expression) # Extracts the first part of text that matches regular_expression. >>> REGEXEXTRACT(\"Google Doc 101\", \"[0-9]+\") '101' >>> REGEXEXTRACT(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") '826.25' If there is a parenthesized expression, it is returned instead of the whole match. >>> REGEXEXTRACT(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") 'Content' >>> REGEXEXTRACT(\"Foo\", \"Bar\") Traceback (most recent call last): ... ValueError: REGEXEXTRACT text does not match REGEXMATCH # REGEXMATCH (text, regular_expression) # Returns whether a piece of text matches a regular expression. >>> REGEXMATCH(\"Google Doc 101\", \"[0-9]+\") True >>> REGEXMATCH(\"Google Doc\", \"[0-9]+\") False >>> REGEXMATCH(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") True >>> REGEXMATCH(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") True >>> REGEXMATCH(\"Foo\", \"Bar\") False REGEXREPLACE # REGEXREPLACE (text, regular_expression, replacement) # Replaces all parts of text matching the given regular expression with replacement text. >>> REGEXREPLACE(\"Google Doc 101\", \"[0-9]+\", \"777\") 'Google Doc 777' >>> REGEXREPLACE(\"Google Doc\", \"[0-9]+\", \"777\") 'Google Doc' >>> REGEXREPLACE(\"The price is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\", \"315.75\") 'The price is $315.75' >>> REGEXREPLACE(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\", \"Word\") 'Word between brackets' >>> REGEXREPLACE(\"Foo\", \"Bar\", \"Baz\") 'Foo' REPLACE # REPLACE (text, position, length, new_text) # Replaces part of a text string with a different text string. Position is counted from 1. >>> REPLACE(\"abcdefghijk\", 6, 5, \"*\") 'abcde*k' >>> REPLACE(\"2009\", 3, 2, \"10\") '2010' >>> REPLACE('123456', 1, 3, '@') '@456' >>> REPLACE('foo', 1, 0, 'bar') 'barfoo' >>> REPLACE('foo', 0, 1, 'bar') Traceback (most recent call last): ... ValueError: position invalid REPT # REPT (text, number_times) # Returns specified text repeated a number of times. Same as text * number_times . The result of the REPT function cannot be longer than 32767 characters, or it raises a ValueError. >>> REPT(\"*-\", 3) '*-*-*-' >>> REPT('-', 10) '----------' >>> REPT('-', 0) '' >>> len(REPT('---', 10000)) 30000 >>> REPT('---', 11000) Traceback (most recent call last): ... ValueError: number_times invalid >>> REPT('-', -1) Traceback (most recent call last): ... ValueError: number_times invalid RIGHT # RIGHT (string, num_chars=1) # Returns a substring of length num_chars from the end of a specified string. If num_chars is omitted, it is assumed to be 1. Same as string[-num_chars:] . >>> RIGHT(\"Sale Price\", 5) 'Price' >>> RIGHT('Stock Number') 'r' >>> RIGHT('Text', 100) 'Text' >>> RIGHT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid SEARCH # SEARCH (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text, ignoring case. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> SEARCH(\"e\", \"Statements\", 6) 7 >>> SEARCH(\"margin\", \"Profit Margin\") 8 >>> SEARCH(\" \", \"Profit Margin\") 7 >>> SEARCH('\"', 'The \"boss\" is here.') 5 >>> SEARCH(\"gle\", \"Google\") 4 >>> SEARCH(\"GLE\", \"Google\") 4 SUBSTITUTE # SUBSTITUTE (text, old_text, new_text, instance_num=None) # Replaces existing text with new text in a string. It is useful when you know the substring of text to replace. Use REPLACE when you know the position of text to replace. If instance_num is given, it specifies which occurrence of old_text to replace. If omitted, all occurrences are replaced. Same as text.replace(old_text, new_text) when instance_num is omitted. >>> SUBSTITUTE(\"Sales Data\", \"Sales\", \"Cost\") u'Cost Data' >>> SUBSTITUTE(\"Quarter 1, 2008\", \"1\", \"2\", 1) u'Quarter 2, 2008' >>> SUBSTITUTE(\"Quarter 1, 2011\", \"1\", \"2\", 3) u'Quarter 1, 2012' T # T (value) # Returns value if value is text, or the empty string when value is not text. >>> T('Text') u'Text' >>> T(826) u'' >>> T('826') u'826' >>> T(False) u'' >>> T('100 points') u'100 points' >>> T(AltText('Text')) u'Text' >>> T(float('nan')) u'' TEXT # TEXT (number, format_type) # Converts a number into text according to a specified format. It is not yet implemented in Grist. Note This function is not currently implemented in Grist. TRIM # TRIM (text) # Removes all spaces from text except for single spaces between words. Note that TRIM does not remove other whitespace such as tab or newline characters. >>> TRIM(\" First Quarter\\n Earnings \") 'First Quarter\\n Earnings' >>> TRIM(\"\") '' UPPER # UPPER (text) # Converts a specified string to uppercase. Same as text.upper() . >>> UPPER(\"e. e. cummings\") 'E. E. CUMMINGS' >>> UPPER(\"Apt. 2B\") 'APT. 2B' VALUE # VALUE (text) # Converts a string in accepted date, time or number formats into a number or date. >>> VALUE(\"$1,000\") 1000 >>> assert VALUE(\"16:48:00\") - VALUE(\"12:00:00\") == datetime.timedelta(0, 17280) >>> VALUE(\"01/01/2012\") datetime.datetime(2012, 1, 1, 0, 0) >>> VALUE(\"\") 0 >>> VALUE(0) 0 >>> VALUE(\"826\") 826 >>> VALUE(\"-826.123123123\") -826.123123123 >>> VALUE(float('nan')) nan >>> VALUE(\"Invalid\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number >>> VALUE(\"13/13/13\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number","title":"Function reference"},{"location":"functions/#function-reference","text":"Grist formulas support most Excel functions, as well as the Python programming language. The table below lists Grist-specific functions, and the suite of the included Excel-like functions. In addition, the entire Python standard library is available. For more about using formulas in Grist, see Intro to Formulas . Grist uses Python (version 3.11) for formulas. You can use nearly all features of Python (see Python documentation ). Here are some helpful notes: Python is case-sensitive, including for Grist table and column names. Excel-like functions are always in uppercase. E.g. if is a Python keyword, while IF is an Excel-like function. Compare for equality using == , in place of Excel\u2019s single = (which in Python means assignment). \u201cNot equal\u201d uses != in place of Excel\u2019s <> . You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. Category Functions Grist Record or rec , $Field or rec.Field , $group or rec.group , RecordSet , UserTable , all , lookupOne , lookupRecords Date DATE , DATEADD , DATEDIF , DATEVALUE , DATE_TO_XL , DAY , DAYS , DTIME , EDATE , EOMONTH , HOUR , ISOWEEKNUM , MINUTE , MONTH , NOW , SECOND , TODAY , WEEKDAY , WEEKNUM , XL_TO_DATE , YEAR , YEARFRAC Info CELL , CURRENT_CONVERSION , ISBLANK , ISEMAIL , ISERR , ISERROR , ISLOGICAL , ISNA , ISNONTEXT , ISNUMBER , ISREF , ISREFLIST , ISTEXT , ISURL , N , NA , PEEK , RECORD , REQUEST , TYPE Logical AND , FALSE , IF , IFERROR , NOT , OR , TRUE Lookup lookupOne , lookupRecords , ADDRESS , CHOOSE , COLUMN , COLUMNS , CONTAINS , GETPIVOTDATA , HLOOKUP , HYPERLINK , INDEX , INDIRECT , LOOKUP , MATCH , OFFSET , ROW , ROWS , SELF_HYPERLINK , VLOOKUP Math ABS , ACOS , ACOSH , ARABIC , ASIN , ASINH , ATAN , ATAN2 , ATANH , CEILING , COMBIN , COS , COSH , DEGREES , EVEN , EXP , FACT , FACTDOUBLE , FLOOR , GCD , INT , LCM , LN , LOG , LOG10 , MOD , MROUND , MULTINOMIAL , ODD , PI , POWER , PRODUCT , QUOTIENT , RADIANS , RAND , RANDBETWEEN , ROMAN , ROUND , ROUNDDOWN , ROUNDUP , SERIESSUM , SIGN , SIN , SINH , SQRT , SQRTPI , SUBTOTAL , SUM , SUMIF , SUMIFS , SUMPRODUCT , SUMSQ , TAN , TANH , TRUNC , UUID Schedule SCHEDULE Stats AVEDEV , AVERAGE , AVERAGEA , AVERAGEIF , AVERAGEIFS , AVERAGE_WEIGHTED , BINOMDIST , CONFIDENCE , CORREL , COUNT , COUNTA , COVAR , CRITBINOM , DEVSQ , EXPONDIST , FDIST , FISHER , FISHERINV , FORECAST , F_DIST , F_DIST_RT , GEOMEAN , HARMEAN , HYPGEOMDIST , INTERCEPT , KURT , LARGE , LOGINV , LOGNORMDIST , MAX , MAXA , MEDIAN , MIN , MINA , MODE , NEGBINOMDIST , NORMDIST , NORMINV , NORMSDIST , NORMSINV , PEARSON , PERCENTILE , PERCENTRANK , PERCENTRANK_EXC , PERCENTRANK_INC , PERMUT , POISSON , PROB , QUARTILE , RANK , RANK_AVG , RANK_EQ , RSQ , SKEW , SLOPE , SMALL , STANDARDIZE , STDEV , STDEVA , STDEVP , STDEVPA , STEYX , TDIST , TINV , TRIMMEAN , TTEST , T_INV , T_INV_2T , VAR , VARA , VARP , VARPA , WEIBULL , ZTEST Text CHAR , CLEAN , CODE , CONCAT , CONCATENATE , DOLLAR , EXACT , FIND , FIXED , LEFT , LEN , LOWER , MID , PHONE_FORMAT , PROPER , REGEXEXTRACT , REGEXMATCH , REGEXREPLACE , REPLACE , REPT , RIGHT , SEARCH , SUBSTITUTE , T , TEXT , TRIM , UPPER , VALUE","title":""},{"location":"functions/#grist","text":"","title":"Grist"},{"location":"functions/#record","text":"class Record # A Record represents a record of data. It is the primary means of accessing values in formulas. A Record for a particular table has a property for each data and formula column in the table. In a formula, $field is translated to rec.field , where rec is the Record for which the formula is being evaluated. For example: def Full_Name(rec, table): return rec.First_Name + ' ' + rec.LastName def Name_Length(rec, table): return len(rec.Full_Name)","title":"Record"},{"location":"functions/#field","text":"$ Field or rec .Field # Access the field named \u201cField\u201d of the current record. E.g. $First_Name or rec.First_Name .","title":"$Field"},{"location":"functions/#group","text":"$group # In a summary table , $group is a special field containing the list of Records that are summarized by the current summary line. E.g. the formula len($group) counts the number of those records being summarized in each row. See RecordSet for useful properties offered by the returned object. Examples: sum($group.Amount) # Sum of the Amount field in the matching records sum(r.Amount for r in $group) # Same as sum($group.Amount) sum(r.Amount for r in $group if r > 0) # Sum of only the positive amounts sum(r.Shares * r.Price for r in $group) # Sum of shares * price products","title":"$group"},{"location":"functions/#recordset","text":"class RecordSet # A RecordSet represents a collection of records, as returned by Table.lookupRecords() or $group property in summary views. A RecordSet allows iterating through the records: sum(r.Amount for r in Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\")) min(r.DueDate for r in Tasks.lookupRecords(Owner=\"Bob\")) RecordSets also provide a convenient way to access the list of values for a particular field for all the records, as record_set.Field . For example, the examples above are equivalent to: sum(Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\").Amount) min(Tasks.lookupRecords(Owner=\"Bob\").DueDate) You can get the number of records in a RecordSet using len , e.g. len($group) .","title":"RecordSet"},{"location":"functions/#usertable","text":"class UserTable # Each data table in the document is represented in the code by an instance of UserTable class. These names are always capitalized. A UserTable provides access to all the records in the table, as well as methods to look up particular records. Every table in the document is available to all formulas.","title":"UserTable"},{"location":"functions/#all","text":"UserTable. all # The list of all the records in this table. For example, this evaluates to the number of records in the table Students . len(Students.all) This evaluates to the sum of the Population field for every record in the table Countries . sum(r.Population for r in Countries.all)","title":"all"},{"location":"functions/#lookupone","text":"UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. If sort_by=field is given, sort the results by that field. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email) People.lookupOne(Email=$Work_Email, sort_by=\"Date\")","title":"lookupOne"},{"location":"functions/#lookuprecords","text":"UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value.","title":"lookupRecords"},{"location":"functions/#date","text":"","title":"Date"},{"location":"functions/#date_1","text":"DATE (year, month, day) # Returns the datetime.datetime object that represents a particular date. The DATE function is most useful in formulas where year, month, and day are formulas, not constants. If year is between 0 and 1899 (inclusive), adds 1900 to calculate the year. >>> DATE(108, 1, 2) datetime.date(2008, 1, 2) >>> DATE(2008, 1, 2) datetime.date(2008, 1, 2) If month is greater than 12, rolls into the following year. >>> DATE(2008, 14, 2) datetime.date(2009, 2, 2) If month is less than 1, subtracts that many months plus 1, from the first month in the year. >>> DATE(2008, -3, 2) datetime.date(2007, 9, 2) If day is greater than the number of days in the given month, rolls into the following months. >>> DATE(2008, 1, 35) datetime.date(2008, 2, 4) If day is less than 1, subtracts that many days plus 1, from the first day of the given month. >>> DATE(2008, 1, -15) datetime.date(2007, 12, 16)","title":"DATE"},{"location":"functions/#dateadd","text":"DATEADD (start_date, days=0, months=0, years=0, weeks=0) # Returns the date a given number of days, months, years, or weeks away from start_date . You may specify arguments in any order if you specify argument names. Use negative values to subtract. For example, DATEADD(date, 1) is the same as DATEADD(date, days=1) , ands adds one day to date . DATEADD(date, years=1, days=-1) adds one year minus one day. >>> DATEADD(DATE(2011, 1, 15), 1) datetime.date(2011, 1, 16) >>> DATEADD(DATE(2011, 1, 15), months=1, days=-1) datetime.date(2011, 2, 14) >>> DATEADD(DATE(2011, 1, 15), years=-2, months=1, days=3, weeks=2) datetime.date(2009, 3, 4) >>> DATEADD(DATE(1975, 4, 30), years=50, weeks=-5) datetime.date(2025, 3, 26)","title":"DATEADD"},{"location":"functions/#datedif","text":"DATEDIF (start_date, end_date, unit) # Calculates the number of days, months, or years between two dates. Unit indicates the type of information that you want returned: \u201cY\u201d: The number of complete years in the period. \u201cM\u201d: The number of complete months in the period. \u201cD\u201d: The number of days in the period. \u201cMD\u201d: The difference between the days in start_date and end_date. The months and years of the dates are ignored. \u201cYM\u201d: The difference between the months in start_date and end_date. The days and years of the dates are ignored. \u201cYD\u201d: The difference between the days of start_date and end_date. The years of the dates are ignored. Two complete years in the period (2) >>> DATEDIF(DATE(2001, 1, 1), DATE(2003, 1, 1), \"Y\") 2 440 days between June 1, 2001, and August 15, 2002 (440) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"D\") 440 75 days between June 1 and August 15, ignoring the years of the dates (75) >>> DATEDIF(DATE(2001, 6, 1), DATE(2012, 8, 15), \"YD\") 75 The difference between 1 and 15, ignoring the months and the years of the dates (14) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"MD\") 14","title":"DATEDIF"},{"location":"functions/#datevalue","text":"DATEVALUE (date_string, tz=None) # Converts a date that is stored as text to a datetime object. >>> DATEVALUE(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"30-Jan-2008\") datetime.datetime(2008, 1, 30, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"2008-12-11\") datetime.datetime(2008, 12, 11, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"5-JUL\").replace(year=2000) datetime.datetime(2000, 7, 5, 0, 0, tzinfo=moment.tzinfo('America/New_York')) In case of ambiguity, prefer M/D/Y format. >>> DATEVALUE(\"1/2/3\") datetime.datetime(2003, 1, 2, 0, 0, tzinfo=moment.tzinfo('America/New_York'))","title":"DATEVALUE"},{"location":"functions/#date_to_xl","text":"DATE_TO_XL (date_value) # Converts a Python date or datetime object to the serial number as used by Excel, with December 30, 1899 as serial number 1. See XL_TO_DATE for more explanation. >>> DATE_TO_XL(datetime.date(2008, 1, 1)) 39448.0 >>> DATE_TO_XL(datetime.date(2012, 3, 14)) 40982.0 >>> DATE_TO_XL(datetime.datetime(2012, 3, 14, 1, 30)) 40982.0625","title":"DATE_TO_XL"},{"location":"functions/#day","text":"DAY (date) # Returns the day of a date, as an integer ranging from 1 to 31. Same as date.day . >>> DAY(DATE(2011, 4, 15)) 15 >>> DAY(\"5/31/2012\") 31 >>> DAY(datetime.datetime(1900, 1, 1)) 1","title":"DAY"},{"location":"functions/#days","text":"DAYS (end_date, start_date) # Returns the number of days between two dates. Same as (end_date - start_date).days . >>> DAYS(\"3/15/11\",\"2/1/11\") 42 >>> DAYS(DATE(2011, 12, 31), DATE(2011, 1, 1)) 364 >>> DAYS(\"2/1/11\", \"3/15/11\") -42","title":"DAYS"},{"location":"functions/#dtime","text":"DTIME (value, tz=None) # Returns the value converted to a python datetime object. The value may be a string , date (interpreted as midnight on that day), time (interpreted as a time-of-day today), or an existing datetime . The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. If the input is itself a datetime with the timezone set, it is returned unchanged (no changes to its timezone). >>> DTIME(datetime.date(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.date(2017, 1, 1), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('Europe/Paris')) >>> DTIME(datetime.datetime(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC'))) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC')), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))","title":"DTIME"},{"location":"functions/#edate","text":"EDATE (start_date, months) # Returns the date that is the given number of months before or after start_date . Use EDATE to calculate maturity dates or due dates that fall on the same day of the month as the date of issue. >>> EDATE(DATE(2011, 1, 15), 1) datetime.date(2011, 2, 15) >>> EDATE(DATE(2011, 1, 15), -1) datetime.date(2010, 12, 15) >>> EDATE(DATE(2011, 1, 15), 2) datetime.date(2011, 3, 15) >>> EDATE(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 1) >>> EDATE(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 1)","title":"EDATE"},{"location":"functions/#eomonth","text":"EOMONTH (start_date, months) # Returns the date for the last day of the month that is the indicated number of months before or after start_date. Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. >>> EOMONTH(DATE(2011, 1, 1), 1) datetime.date(2011, 2, 28) >>> EOMONTH(DATE(2011, 1, 15), -3) datetime.date(2010, 10, 31) >>> EOMONTH(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 31) >>> EOMONTH(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 31)","title":"EOMONTH"},{"location":"functions/#hour","text":"HOUR (time) # Same as time.hour . >>> HOUR(XL_TO_DATE(0.75)) 18 >>> HOUR(\"7/18/2011 7:45\") 7 >>> HOUR(\"4/21/2012\") 0","title":"HOUR"},{"location":"functions/#isoweeknum","text":"ISOWEEKNUM (date) # Returns the ISO week number of the year for a given date. >>> ISOWEEKNUM(\"3/9/2012\") 10 >>> [ISOWEEKNUM(DATE(2000 + y, 1, 1)) for y in [0,1,2,3,4,5,6,7,8]] [52, 1, 1, 1, 1, 53, 52, 1, 1]","title":"ISOWEEKNUM"},{"location":"functions/#minute","text":"MINUTE (time) # Returns the minutes of datetime , as an integer from 0 to 59. Same as time.minute . >>> MINUTE(XL_TO_DATE(0.75)) 0 >>> MINUTE(\"7/18/2011 7:45\") 45 >>> MINUTE(\"12:59:00 PM\") 59 >>> MINUTE(datetime.time(12, 58, 59)) 58","title":"MINUTE"},{"location":"functions/#month","text":"MONTH (date) # Returns the month of a date represented, as an integer from from 1 (January) to 12 (December). Same as date.month . >>> MONTH(DATE(2011, 4, 15)) 4 >>> MONTH(\"5/31/2012\") 5 >>> MONTH(datetime.datetime(1900, 1, 1)) 1","title":"MONTH"},{"location":"functions/#now","text":"NOW (tz=None) # Returns the datetime object for the current time.","title":"NOW"},{"location":"functions/#second","text":"SECOND (time) # Returns the seconds of datetime , as an integer from 0 to 59. Same as time.second . >>> SECOND(XL_TO_DATE(0.75)) 0 >>> SECOND(\"7/18/2011 7:45:13\") 13 >>> SECOND(datetime.time(12, 58, 59)) 59","title":"SECOND"},{"location":"functions/#today","text":"TODAY (tz=None) # Returns the date object for the current date.","title":"TODAY"},{"location":"functions/#weekday","text":"WEEKDAY (date, return_type=1) # Returns the day of the week corresponding to a date. The day is given as an integer, ranging from 1 (Sunday) to 7 (Saturday), by default. Return_type determines the type of the returned value. 1 (default) - Returns 1 (Sunday) through 7 (Saturday). 2 - Returns 1 (Monday) through 7 (Sunday). 3 - Returns 0 (Monday) through 6 (Sunday). 11 - Returns 1 (Monday) through 7 (Sunday). 12 - Returns 1 (Tuesday) through 7 (Monday). 13 - Returns 1 (Wednesday) through 7 (Tuesday). 14 - Returns 1 (Thursday) through 7 (Wednesday). 15 - Returns 1 (Friday) through 7 (Thursday). 16 - Returns 1 (Saturday) through 7 (Friday). 17 - Returns 1 (Sunday) through 7 (Saturday). >>> WEEKDAY(DATE(2008, 2, 14)) 5 >>> WEEKDAY(DATE(2012, 3, 1)) 5 >>> WEEKDAY(DATE(2012, 3, 1), 1) 5 >>> WEEKDAY(DATE(2012, 3, 1), 2) 4 >>> WEEKDAY(\"3/1/2012\", 3) 3","title":"WEEKDAY"},{"location":"functions/#weeknum","text":"WEEKNUM (date, return_type=1) # Returns the week number of a specific date. For example, the week containing January 1 is the first week of the year, and is numbered week 1. Return_type determines which week is considered the first week of the year. 1 (default) - Week 1 is the first week starting Sunday that contains January 1. 2 - Week 1 is the first week starting Monday that contains January 1. 11 - Week 1 is the first week starting Monday that contains January 1. 12 - Week 1 is the first week starting Tuesday that contains January 1. 13 - Week 1 is the first week starting Wednesday that contains January 1. 14 - Week 1 is the first week starting Thursday that contains January 1. 15 - Week 1 is the first week starting Friday that contains January 1. 16 - Week 1 is the first week starting Saturday that contains January 1. 17 - Week 1 is the first week starting Sunday that contains January 1. 21 - ISO 8601 Approach: Week 1 is the first week starting Monday that contains January 4. Equivalently, it is the week that contains the first Thursday of the year. >>> WEEKNUM(DATE(2012, 3, 9)) 10 >>> WEEKNUM(DATE(2012, 3, 9), 2) 11 >>> WEEKNUM('1/1/1900') 1 >>> WEEKNUM('2/1/1900') 5","title":"WEEKNUM"},{"location":"functions/#xl_to_date","text":"XL_TO_DATE (value, tz=None) # Converts a provided Excel serial number representing a date into a datetime object. Value is interpreted as the number of days since December 30, 1899. (This corresponds to Google Sheets interpretation. Excel starts with Dec. 31, 1899 but wrongly considers 1900 to be a leap year. Excel for Mac should be configured to use 1900 date system, i.e. uncheck \u201cUse the 1904 date system\u201d option.) The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. >>> XL_TO_DATE(41100.1875) datetime.datetime(2012, 7, 10, 4, 30, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(39448) datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(40982.0625) datetime.datetime(2012, 3, 14, 1, 30, tzinfo=moment.tzinfo('America/New_York'))","title":"XL_TO_DATE"},{"location":"functions/#year","text":"YEAR (date) # Returns the year corresponding to a date as an integer. Same as date.year . >>> YEAR(DATE(2011, 4, 15)) 2011 >>> YEAR(\"5/31/2030\") 2030 >>> YEAR(datetime.datetime(1900, 1, 1)) 1900","title":"YEAR"},{"location":"functions/#yearfrac","text":"YEARFRAC (start_date, end_date, basis=0) # Calculates the fraction of the year represented by the number of whole days between two dates. Basis is the type of day count basis to use. 0 (default) - US (NASD) 30/360 1 - Actual/actual 2 - Actual/360 3 - Actual/365 4 - European 30/360 -1 - Actual/actual (Google Sheets variation) This function is useful for financial calculations. For compatibility with Excel, it defaults to using the NASD standard calendar. For use in non-financial settings, option -1 is likely the best choice. See https://en.wikipedia.org/wiki/360-day_calendar for explanation of the US 30/360 and European 30/360 methods. See http://www.dwheeler.com/yearfrac/ for analysis of Excel\u2019s particular implementation. Basis -1 is similar to 1 , but differs from Excel when dates span both leap and non-leap years. It matches the calculation in Google Sheets, counting the days in each year as a fraction of that year\u2019s length. Fraction of the year between 1/1/2012 and 7/30/12, omitting the Basis argument. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30)) '0.58055556' Fraction between same dates, using the Actual/Actual basis argument. Because 2012 is a Leap year, it has a 366 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 1) '0.57650273' Fraction between same dates, using the Actual/365 basis argument. Uses a 365 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 3) '0.57808219'","title":"YEARFRAC"},{"location":"functions/#info","text":"","title":"Info"},{"location":"functions/#cell","text":"CELL (info_type, reference) # Returns the requested information about the specified cell. This is not implemented in Grist Note This function is not currently implemented in Grist.","title":"CELL"},{"location":"functions/#current_conversion","text":"CURRENT_CONVERSION (rec) # Internal function used by Grist during column type conversions. Not available for use in formulas.","title":"CURRENT_CONVERSION"},{"location":"functions/#isblank","text":"ISBLANK (value) # Returns whether a value refers to an empty cell. It isn\u2019t implemented in Grist. To check for an empty string, use value == \"\" . Note This function is not currently implemented in Grist.","title":"ISBLANK"},{"location":"functions/#isemail","text":"ISEMAIL (value) # Returns whether a value is a valid email address. Note that checking email validity is not an exact science. The technical standard considers many email addresses valid that are not used in practice, and would not be considered valid by most users. Instead, we follow Google Sheets implementation, with some differences, noted below. >>> ISEMAIL(\"Abc.123@example.com\") True >>> ISEMAIL(\"Bob_O-Reilly+tag@example.com\") True >>> ISEMAIL(\"John Doe\") False >>> ISEMAIL(\"john@aol...com\") False","title":"ISEMAIL"},{"location":"functions/#iserr","text":"ISERR (value) # Checks whether a value is an error. In other words, it returns true if using value directly would raise an exception. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. A more Pythonic approach to checking for errors is: try: ... value ... except Exception, err: ... do something about the error ... For example: >>> ISERR(\"Hello\") False","title":"ISERR"},{"location":"functions/#iserror","text":"ISERROR (value) # Checks whether a value is an error or an invalid value. It is similar to ISERR , but also returns true for an invalid value such as NaN or a text value in a Numeric column. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. >>> ISERROR(\"Hello\") False >>> ISERROR(AltText(\"fail\")) True >>> ISERROR(float('nan')) True","title":"ISERROR"},{"location":"functions/#islogical","text":"ISLOGICAL (value) # Checks whether a value is True or False . >>> ISLOGICAL(True) True >>> ISLOGICAL(False) True >>> ISLOGICAL(0) False >>> ISLOGICAL(None) False >>> ISLOGICAL(\"Test\") False","title":"ISLOGICAL"},{"location":"functions/#isna","text":"ISNA (value) # Checks whether a value is the error #N/A . >>> ISNA(float('nan')) True >>> ISNA(0.0) False >>> ISNA('text') False >>> ISNA(float('-inf')) False","title":"ISNA"},{"location":"functions/#isnontext","text":"ISNONTEXT (value) # Checks whether a value is non-textual. >>> ISNONTEXT(\"asdf\") False >>> ISNONTEXT(\"\") False >>> ISNONTEXT(AltText(\"text\")) False >>> ISNONTEXT(17.0) True >>> ISNONTEXT(None) True >>> ISNONTEXT(datetime.date(2011, 1, 1)) True","title":"ISNONTEXT"},{"location":"functions/#isnumber","text":"ISNUMBER (value) # Checks whether a value is a number. >>> ISNUMBER(17) True >>> ISNUMBER(-123.123423) True >>> ISNUMBER(False) True >>> ISNUMBER(float('nan')) True >>> ISNUMBER(float('inf')) True >>> ISNUMBER('17') False >>> ISNUMBER(None) False >>> ISNUMBER(datetime.date(2011, 1, 1)) False","title":"ISNUMBER"},{"location":"functions/#isref","text":"ISREF (value) # Checks whether a value is a table record. For example, if a column person is of type Reference to the People table, then ISREF($person) is True . Similarly, ISREF(People.lookupOne(name=$name)) is True . For any other type of value, ISREF() would evaluate to False . >>> ISREF(17) False >>> ISREF(\"Roger\") False","title":"ISREF"},{"location":"functions/#isreflist","text":"ISREFLIST (value) # Checks whether a value is a RecordSet , the type of values in Reference List columns. For example, if a column people is of type Reference List to the People table, then ISREFLIST($people) is True . Similarly, ISREFLIST(People.lookupRecords(name=$name)) is True . For any other type of value, ISREFLIST() would evaluate to False . >>> ISREFLIST(17) False >>> ISREFLIST(\"Roger\") False","title":"ISREFLIST"},{"location":"functions/#istext","text":"ISTEXT (value) # Checks whether a value is text. >>> ISTEXT(\"asdf\") True >>> ISTEXT(\"\") True >>> ISTEXT(AltText(\"text\")) True >>> ISTEXT(17.0) False >>> ISTEXT(None) False >>> ISTEXT(datetime.date(2011, 1, 1)) False","title":"ISTEXT"},{"location":"functions/#isurl","text":"ISURL (value) # Checks whether a value is a valid URL. It does not need to be fully qualified, or to include \u201chttp://\u201d and \u201cwww\u201d. It does not follow a standard, but attempts to work similarly to ISURL in Google Sheets, and to return True for text that is likely a URL. Valid protocols include ftp, http, https, gopher, mailto, news, telnet, and aim. >>> ISURL(\"http://www.getgrist.com\") True >>> ISURL(\"https://foo.com/test_(wikipedia)#cite-1\") True >>> ISURL(\"mailto://user@example.com\") True >>> ISURL(\"http:///a\") False","title":"ISURL"},{"location":"functions/#n","text":"N (value) # Returns the value converted to a number. True/False are converted to 1/0. A date is converted to Excel-style serial number of the date. Anything else is converted to 0. >>> N(7) 7 >>> N(7.1) 7.1 >>> N(\"Even\") 0 >>> N(\"7\") 0 >>> N(True) 1 >>> N(datetime.datetime(2011, 4, 17)) 40650.0","title":"N"},{"location":"functions/#na","text":"NA () # Returns the \u201cvalue not available\u201d error, #N/A . >>> math.isnan(NA()) True","title":"NA"},{"location":"functions/#peek","text":"PEEK (func) # Evaluates the given expression without creating dependencies or requiring that referenced values are up to date, using whatever value it finds in a cell. This is useful for preventing circular reference errors, particularly in trigger formulas. For example, if the formula for A depends on $B and the formula for B depends on $A , then normally this would raise a circular reference error because each value needs to be calculated before the other. But if A uses PEEK($B) then it will simply get the value already stored in $B without requiring that $B is first calculated to the latest value. Therefore A will be calculated first, and B can use $A without problems.","title":"PEEK"},{"location":"functions/#record_1","text":"RECORD (record_or_list, dates_as_iso=False, expand_refs=0) # Returns a Python dictionary with all fields in the given record. If a list of records is given, returns a list of corresponding Python dictionaries. If dates_as_iso is set, Date and DateTime values are converted to string using ISO 8601 format. If expand_refs is set to 1 or higher, Reference values are replaced with a RECORD representation of the referenced record, expanding the given number of levels. Error values present in cells of the record are replaced with None value, and a special key of \u201c error \u201d gets added containing the error messages for those cells. For example: {\"Ratio\": None, \"_error_\": {\"Ratio\": \"ZeroDivisionError: integer division or modulo by zero\"}} Note that care is needed to avoid circular references when using RECORD(), since it creates a dependency on every cell in the record. In case of RECORD(rec), the cell containing this call will be omitted from the resulting dictionary. For example: RECORD($Person) RECORD(rec) RECORD(People.lookupOne(First_Name=\"Alice\")) RECORD(People.lookupRecords(Department=\"HR\"))","title":"RECORD"},{"location":"functions/#request","text":"REQUEST (url, params=None, headers=None) # Note This function is not currently implemented in Grist.","title":"REQUEST"},{"location":"functions/#type","text":"TYPE (value) # Returns a number associated with the type of data passed into the function. This is not implemented in Grist. Use isinstance(value, type) or type(value) . Note This function is not currently implemented in Grist.","title":"TYPE"},{"location":"functions/#logical","text":"","title":"Logical"},{"location":"functions/#and","text":"AND (logical_expression, *logical_expressions) # Returns True if all of the arguments are logically true, and False if any are false. Same as all([value1, value2, ...]) . >>> AND(1) True >>> AND(0) False >>> AND(1, 1) True >>> AND(1,2,3,4) True >>> AND(1,2,3,4,0) False","title":"AND"},{"location":"functions/#false","text":"FALSE () # Returns the logical value False . You may also use the value False directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> FALSE() False","title":"FALSE"},{"location":"functions/#if","text":"IF (logical_expression, value_if_true, value_if_false) # Returns one value if a logical expression is True and another if it is False . The equivalent Python expression is: value_if_true if logical_expression else value_if_false Since Grist supports multi-line formulas, you may also use Python blocks such as: if logical_expression: return value_if_true else: return value_if_false NOTE: Grist follows Excel model by only evaluating one of the value expressions, by automatically wrapping the expressions to use lazy evaluation. This allows IF(False, 1/0, 1) to evaluate to 1 rather than raise an exception. >>> IF(12, \"Yes\", \"No\") 'Yes' >>> IF(None, \"Yes\", \"No\") 'No' >>> IF(True, 0.85, 0.0) 0.85 >>> IF(False, 0.85, 0.0) 0.0","title":"IF"},{"location":"functions/#iferror","text":"IFERROR (value, value_if_error=\u2019\u2018) # Returns the first argument if it is not an error value, otherwise returns the second argument if present, or a blank if the second argument is absent. NOTE: Grist handles values that raise an exception by wrapping them to use lazy evaluation. >>> IFERROR(float('nan'), \"**NAN**\") '**NAN**' >>> IFERROR(17.17, \"**NAN**\") 17.17 >>> IFERROR(\"Text\") 'Text' >>> IFERROR(AltText(\"hello\")) ''","title":"IFERROR"},{"location":"functions/#not","text":"NOT (logical_expression) # True . Same as not logical_expression . >>> NOT(123) False >>> NOT(0) True","title":"NOT"},{"location":"functions/#or","text":"OR (logical_expression, *logical_expressions) # Returns True if any of the arguments is logically true, and false if all of the arguments are false. Same as any([value1, value2, ...]) . >>> OR(1) True >>> OR(0) False >>> OR(1, 1) True >>> OR(0, 1) True >>> OR(0, 0) False >>> OR(0,False,0.0,\"\",None) False >>> OR(0,None,3,0) True","title":"OR"},{"location":"functions/#true","text":"TRUE () # Returns the logical value True . You may also use the value True directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> TRUE() True","title":"TRUE"},{"location":"functions/#lookup","text":"","title":"Lookup"},{"location":"functions/#lookupone_1","text":"UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email)","title":"lookupOne"},{"location":"functions/#lookuprecords_1","text":"UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value.","title":"lookupRecords"},{"location":"functions/#address","text":"ADDRESS (row, column, absolute_relative_mode, use_a1_notation, sheet) # Returns a cell reference as a string. Note This function is not currently implemented in Grist.","title":"ADDRESS"},{"location":"functions/#choose","text":"CHOOSE (index, choice1, choice2) # Returns an element from a list of choices based on index. Note This function is not currently implemented in Grist.","title":"CHOOSE"},{"location":"functions/#column","text":"COLUMN (cell_reference=None) # Returns the column number of a specified cell, with A=1 . Note This function is not currently implemented in Grist.","title":"COLUMN"},{"location":"functions/#columns","text":"COLUMNS (range) # Returns the number of columns in a specified array or range. Note This function is not currently implemented in Grist.","title":"COLUMNS"},{"location":"functions/#contains","text":"CONTAINS (value, match_empty=no_match_empty) # Use this marker with UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. For example: MoviesTable.lookupRecords(genre=CONTAINS(\"Drama\")) will return records in MoviesTable where the column genre is a list or other container such as [\"Comedy\", \"Drama\"] , i.e. \"Drama\" in $genre . Note that the column being looked up (e.g. genre ) must have values of a container type such as list, tuple, or set. In particular the values mustn\u2019t be strings, e.g. \"Comedy-Drama\" won\u2019t match even though \"Drama\" in \"Comedy-Drama\" is True in Python. It also won\u2019t match substrings within container elements, e.g. [\"Comedy-Drama\"] . You can optionally pass a second argument match_empty to indicate a value that should be matched against empty lists in the looked up column. For example, given this formula: MoviesTable.lookupRecords(genre=CONTAINS(g, match_empty='')) If g is '' (i.e. equal to match_empty ) then the column genre in the returned records will either be an empty list (or other container) or a list containing g as usual.","title":"CONTAINS"},{"location":"functions/#getpivotdata","text":"GETPIVOTDATA (value_name, any_pivot_table_cell, original_column_1, pivot_item_1=None, *args) # Extracts an aggregated value from a pivot table that corresponds to the specified row and column headings. Note This function is not currently implemented in Grist.","title":"GETPIVOTDATA"},{"location":"functions/#hlookup","text":"HLOOKUP (search_key, range, index, is_sorted) # Horizontal lookup. Searches across the first row of a range for a key and returns the value of a specified cell in the column found. Note This function is not currently implemented in Grist.","title":"HLOOKUP"},{"location":"functions/#hyperlink","text":"HYPERLINK (url, link_label) # Creates a hyperlink inside a cell. Note This function is not currently implemented in Grist.","title":"HYPERLINK"},{"location":"functions/#index","text":"INDEX (reference, row, column) # Returns the content of a cell, specified by row and column offset. Note This function is not currently implemented in Grist.","title":"INDEX"},{"location":"functions/#indirect","text":"INDIRECT (cell_reference_as_string) # Returns a cell reference specified by a string. Note This function is not currently implemented in Grist.","title":"INDIRECT"},{"location":"functions/#lookup_1","text":"LOOKUP (search_key, search_range_or_search_result_array, result_range=None) # Looks through a row or column for a key and returns the value of the cell in a result range located in the same position as the search row or column. Note This function is not currently implemented in Grist.","title":"LOOKUP"},{"location":"functions/#match","text":"MATCH (search_key, range, search_type) # Returns the relative position of an item in a range that matches a specified value. Note This function is not currently implemented in Grist.","title":"MATCH"},{"location":"functions/#offset","text":"OFFSET (cell_reference, offset_rows, offset_columns, height, width) # Returns a range reference shifted a specified number of rows and columns from a starting cell reference. Note This function is not currently implemented in Grist.","title":"OFFSET"},{"location":"functions/#row","text":"ROW (cell_reference) # Returns the row number of a specified cell. Note This function is not currently implemented in Grist.","title":"ROW"},{"location":"functions/#rows","text":"ROWS (range) # Returns the number of rows in a specified array or range. Note This function is not currently implemented in Grist.","title":"ROWS"},{"location":"functions/#self_hyperlink","text":"SELF_HYPERLINK (label=None, page=None, **kwargs) # Creates a link to the current document. All parameters are optional. The returned string is in URL format, optionally preceded by a label and a space (the format expected for Grist Text columns with the HyperLink option enabled). A numeric page number can be supplied, which will create a link to the specified page. To find the numeric page number you need, visit a page and examine its URL for a /p/NN part. Any number of arguments of the form LinkKey_NAME may be provided, to set user.LinkKey.NAME values that will be available in access rules. For example, if a rule allows users to view rows when user.LinkKey.Code == rec.Code , we might want to create links with SELF_HYPERLINK(LinkKey_Code=$Code) . >>> SELF_HYPERLINK() u'https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(label='doc') u'doc https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(page=2) u'https://docs.getgrist.com/sbaltsirg/Example/p/2' >>> SELF_HYPERLINK(LinkKey_Code='X1234') u'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234' >>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo') u'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo' >>> SELF_HYPERLINK(Linky_Link='Link') Traceback (most recent call last): ... TypeError: unexpected keyword argument 'Linky_Link' (not of form LinkKey_NAME)","title":"SELF_HYPERLINK"},{"location":"functions/#vlookup","text":"VLOOKUP (table, **field_value_pairs) # Vertical lookup. Searches the given table for a record matching the given field=value arguments. If multiple records match, returns one of them. If none match, returns the special empty record. The returned object is a record whose fields are available using .field syntax. For example, VLOOKUP(Employees, EmployeeID=$EmpID).Salary . Note that VLOOKUP isn\u2019t commonly needed in Grist, since Reference columns are the best way to link data between tables, and allow simple efficient usage such as $Person.Age . VLOOKUP is exactly quivalent to table.lookupOne(**field_value_pairs) . See lookupOne . For example: VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\") VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\").Age","title":"VLOOKUP"},{"location":"functions/#math","text":"","title":"Math"},{"location":"functions/#abs","text":"ABS (value) # Returns the absolute value of a number. >>> ABS(2) 2 >>> ABS(-2) 2 >>> ABS(-4) 4","title":"ABS"},{"location":"functions/#acos","text":"ACOS (value) # Returns the inverse cosine of a value, in radians. >>> round(ACOS(-0.5), 9) 2.094395102 >>> round(ACOS(-0.5)*180/PI(), 10) 120.0","title":"ACOS"},{"location":"functions/#acosh","text":"ACOSH (value) # Returns the inverse hyperbolic cosine of a number. >>> ACOSH(1) 0.0 >>> round(ACOSH(10), 7) 2.9932228","title":"ACOSH"},{"location":"functions/#arabic","text":"ARABIC (roman_numeral) # Computes the value of a Roman numeral. >>> ARABIC(\"LVII\") 57 >>> ARABIC('mcmxii') 1912","title":"ARABIC"},{"location":"functions/#asin","text":"ASIN (value) # Returns the inverse sine of a value, in radians. >>> round(ASIN(-0.5), 9) -0.523598776 >>> round(ASIN(-0.5)*180/PI(), 10) -30.0 >>> round(DEGREES(ASIN(-0.5)), 10) -30.0","title":"ASIN"},{"location":"functions/#asinh","text":"ASINH (value) # Returns the inverse hyperbolic sine of a number. >>> round(ASINH(-2.5), 9) -1.647231146 >>> round(ASINH(10), 9) 2.99822295","title":"ASINH"},{"location":"functions/#atan","text":"ATAN (value) # Returns the inverse tangent of a value, in radians. >>> round(ATAN(1), 9) 0.785398163 >>> ATAN(1)*180/PI() 45.0 >>> DEGREES(ATAN(1)) 45.0","title":"ATAN"},{"location":"functions/#atan2","text":"ATAN2 (x, y) # Returns the angle between the x-axis and a line segment from the origin (0,0) to specified coordinate pair ( x , y ), in radians. >>> round(ATAN2(1, 1), 9) 0.785398163 >>> round(ATAN2(-1, -1), 9) -2.35619449 >>> ATAN2(-1, -1)*180/PI() -135.0 >>> DEGREES(ATAN2(-1, -1)) -135.0 >>> round(ATAN2(1,2), 9) 1.107148718","title":"ATAN2"},{"location":"functions/#atanh","text":"ATANH (value) # Returns the inverse hyperbolic tangent of a number. >>> round(ATANH(0.76159416), 9) 1.00000001 >>> round(ATANH(-0.1), 9) -0.100335348","title":"ATANH"},{"location":"functions/#ceiling","text":"CEILING (value, factor=1) # Rounds a number up to the nearest multiple of factor, or the nearest integer if the factor is omitted or 1. >>> CEILING(2.5, 1) 3 >>> CEILING(-2.5, -2) -4 >>> CEILING(-2.5, 2) -2 >>> CEILING(1.5, 0.1) 1.5 >>> CEILING(0.234, 0.01) 0.24","title":"CEILING"},{"location":"functions/#combin","text":"COMBIN (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects. >>> COMBIN(8,2) 28 >>> COMBIN(4,2) 6 >>> COMBIN(10,7) 120","title":"COMBIN"},{"location":"functions/#cos","text":"COS (angle) # Returns the cosine of an angle provided in radians. >>> round(COS(1.047), 7) 0.5001711 >>> round(COS(60*PI()/180), 10) 0.5 >>> round(COS(RADIANS(60)), 10) 0.5","title":"COS"},{"location":"functions/#cosh","text":"COSH (value) # Returns the hyperbolic cosine of any real number. >>> round(COSH(4), 6) 27.308233 >>> round(COSH(EXP(1)), 7) 7.6101251","title":"COSH"},{"location":"functions/#degrees","text":"DEGREES (angle) # Converts an angle value in radians to degrees. >>> round(DEGREES(ACOS(-0.5)), 10) 120.0 >>> DEGREES(PI()) 180.0","title":"DEGREES"},{"location":"functions/#even","text":"EVEN (value) # Rounds a number up to the nearest even integer, rounding away from zero. >>> EVEN(1.5) 2 >>> EVEN(3) 4 >>> EVEN(2) 2 >>> EVEN(-1) -2","title":"EVEN"},{"location":"functions/#exp","text":"EXP (exponent) # Returns Euler\u2019s number, e (~2.718) raised to a power. >>> round(EXP(1), 8) 2.71828183 >>> round(EXP(2), 7) 7.3890561","title":"EXP"},{"location":"functions/#fact","text":"FACT (value) # Returns the factorial of a number. >>> FACT(5) 120 >>> FACT(1.9) 1 >>> FACT(0) 1 >>> FACT(1) 1 >>> FACT(-1) Traceback (most recent call last): ... ValueError: factorial() not defined for negative values","title":"FACT"},{"location":"functions/#factdouble","text":"FACTDOUBLE (value) # Returns the \u201cdouble factorial\u201d of a number. >>> FACTDOUBLE(6) 48 >>> FACTDOUBLE(7) 105 >>> FACTDOUBLE(3) 3 >>> FACTDOUBLE(4) 8","title":"FACTDOUBLE"},{"location":"functions/#floor","text":"FLOOR (value, factor=1) # Rounds a number down to the nearest integer multiple of specified significance. >>> FLOOR(3.7,2) 2 >>> FLOOR(-2.5,-2) -2 >>> FLOOR(2.5,-2) Traceback (most recent call last): ... ValueError: factor argument invalid >>> FLOOR(1.58,0.1) 1.5 >>> FLOOR(0.234,0.01) 0.23","title":"FLOOR"},{"location":"functions/#gcd","text":"GCD (value1, *more_values) # Returns the greatest common divisor of one or more integers. >>> GCD(5, 2) 1 >>> GCD(24, 36) 12 >>> GCD(7, 1) 1 >>> GCD(5, 0) 5 >>> GCD(0, 5) 5 >>> GCD(5) 5 >>> GCD(14, 42, 21) 7","title":"GCD"},{"location":"functions/#int","text":"INT (value) # Rounds a number down to the nearest integer that is less than or equal to it. >>> INT(8.9) 8 >>> INT(-8.9) -9 >>> 19.5-INT(19.5) 0.5","title":"INT"},{"location":"functions/#lcm","text":"LCM (value1, *more_values) # Returns the least common multiple of one or more integers. >>> LCM(5, 2) 10 >>> LCM(24, 36) 72 >>> LCM(0, 5) 0 >>> LCM(5) 5 >>> LCM(10, 100) 100 >>> LCM(12, 18) 36 >>> LCM(12, 18, 24) 72","title":"LCM"},{"location":"functions/#ln","text":"LN (value) # Returns the the logarithm of a number, base e (Euler\u2019s number). >>> round(LN(86), 7) 4.4543473 >>> round(LN(2.7182818), 7) 1.0 >>> round(LN(EXP(3)), 10) 3.0","title":"LN"},{"location":"functions/#log","text":"LOG (value, base=10) # Returns the the logarithm of a number given a base. >>> LOG(10) 1.0 >>> LOG(8, 2) 3.0 >>> round(LOG(86, 2.7182818), 7) 4.4543473","title":"LOG"},{"location":"functions/#log10","text":"LOG10 (value) # Returns the the logarithm of a number, base 10. >>> round(LOG10(86), 9) 1.934498451 >>> LOG10(10) 1.0 >>> LOG10(100000) 5.0 >>> LOG10(10**5) 5.0","title":"LOG10"},{"location":"functions/#mod","text":"MOD (dividend, divisor) # Returns the result of the modulo operator, the remainder after a division operation. >>> MOD(3, 2) 1 >>> MOD(-3, 2) 1 >>> MOD(3, -2) -1 >>> MOD(-3, -2) -1","title":"MOD"},{"location":"functions/#mround","text":"MROUND (value, factor) # Rounds one number to the nearest integer multiple of another. >>> MROUND(10, 3) 9 >>> MROUND(-10, -3) -9 >>> round(MROUND(1.3, 0.2), 10) 1.4 >>> MROUND(5, -2) Traceback (most recent call last): ... ValueError: factor argument invalid","title":"MROUND"},{"location":"functions/#multinomial","text":"MULTINOMIAL (value1, *more_values) # Returns the factorial of the sum of values divided by the product of the values\u2019 factorials. >>> MULTINOMIAL(2, 3, 4) 1260 >>> MULTINOMIAL(3) 1 >>> MULTINOMIAL(1,2,3) 60 >>> MULTINOMIAL(0,2,4,6) 13860","title":"MULTINOMIAL"},{"location":"functions/#odd","text":"ODD (value) # Rounds a number up to the nearest odd integer. >>> ODD(1.5) 3 >>> ODD(3) 3 >>> ODD(2) 3 >>> ODD(-1) -1 >>> ODD(-2) -3","title":"ODD"},{"location":"functions/#pi","text":"PI () # Returns the value of Pi to 14 decimal places. >>> round(PI(), 9) 3.141592654 >>> round(PI()/2, 9) 1.570796327 >>> round(PI()*9, 8) 28.27433388","title":"PI"},{"location":"functions/#power","text":"POWER (base, exponent) # Returns a number raised to a power. >>> POWER(5,2) 25.0 >>> round(POWER(98.6,3.2), 3) 2401077.222 >>> round(POWER(4,5.0/4), 9) 5.656854249","title":"POWER"},{"location":"functions/#product","text":"PRODUCT (factor1, *more_factors) # Returns the result of multiplying a series of numbers together. Each argument may be a number or an array. >>> PRODUCT([5,15,30]) 2250 >>> PRODUCT([5,15,30], 2) 4500 >>> PRODUCT(5,15,[30],[2]) 4500","title":"PRODUCT"},{"location":"functions/#quotient","text":"QUOTIENT (dividend, divisor) # Returns one number divided by another, without the remainder. >>> QUOTIENT(5, 2) 2 >>> QUOTIENT(4.5, 3.1) 1 >>> QUOTIENT(-10, 3) -3","title":"QUOTIENT"},{"location":"functions/#radians","text":"RADIANS (angle) # Converts an angle value in degrees to radians. >>> round(RADIANS(270), 6) 4.712389","title":"RADIANS"},{"location":"functions/#rand","text":"RAND () # Returns a random number between 0 inclusive and 1 exclusive.","title":"RAND"},{"location":"functions/#randbetween","text":"RANDBETWEEN (low, high) # Returns a uniformly random integer between two values, inclusive.","title":"RANDBETWEEN"},{"location":"functions/#roman","text":"ROMAN (number, form_unused=None) # Formats a number in Roman numerals. The second argument is ignored in this implementation. >>> ROMAN(499,0) 'CDXCIX' >>> ROMAN(499.2,0) 'CDXCIX' >>> ROMAN(57) 'LVII' >>> ROMAN(1912) 'MCMXII'","title":"ROMAN"},{"location":"functions/#round","text":"ROUND (value, places=0) # Rounds a number to a certain number of decimal places, by default to the nearest whole number if the number of places is not given. Rounds away from zero (\u2018up\u2019 for positive numbers) in the case of a tie, i.e. when the last digit is 5. >>> ROUND(1.4) 1.0 >>> ROUND(1.5) 2.0 >>> ROUND(2.5) 3.0 >>> ROUND(-2.5) -3.0 >>> ROUND(2.15, 1) 2.2 >>> ROUND(-1.475, 2) -1.48 >>> ROUND(21.5, -1) 20.0 >>> ROUND(626.3,-3) 1000.0 >>> ROUND(1.98,-1) 0.0 >>> ROUND(-50.55,-2) -100.0 >>> ROUND(0) 0.0","title":"ROUND"},{"location":"functions/#rounddown","text":"ROUNDDOWN (value, places=0) # Rounds a number to a certain number of decimal places, always rounding down towards zero. >>> ROUNDDOWN(3.2, 0) 3 >>> ROUNDDOWN(76.9,0) 76 >>> ROUNDDOWN(3.14159, 3) 3.141 >>> ROUNDDOWN(-3.14159, 1) -3.1 >>> ROUNDDOWN(31415.92654, -2) 31400","title":"ROUNDDOWN"},{"location":"functions/#roundup","text":"ROUNDUP (value, places=0) # Rounds a number to a certain number of decimal places, always rounding up away from zero. >>> ROUNDUP(3.2,0) 4 >>> ROUNDUP(76.9,0) 77 >>> ROUNDUP(3.14159, 3) 3.142 >>> ROUNDUP(-3.14159, 1) -3.2 >>> ROUNDUP(31415.92654, -2) 31500","title":"ROUNDUP"},{"location":"functions/#seriessum","text":"SERIESSUM (x, n, m, a) # Given parameters x, n, m, and a, returns the power series sum a_1 x^n + a_2 x^(n+m) + \u2026 + a_i*x^(n+(i-1)m), where i is the number of entries in range a . >>> SERIESSUM(1,0,1,1) 1 >>> SERIESSUM(2,1,0,[1,2,3]) 12 >>> SERIESSUM(-3,1,1,[2,4,6]) -132 >>> round(SERIESSUM(PI()/4,0,2,[1,-1./FACT(2),1./FACT(4),-1./FACT(6)]), 6) 0.707103","title":"SERIESSUM"},{"location":"functions/#sign","text":"SIGN (value) # Given an input number, returns -1 if it is negative, 1 if positive, and 0 if it is zero. >>> SIGN(10) 1 >>> SIGN(4.0-4.0) 0 >>> SIGN(-0.00001) -1","title":"SIGN"},{"location":"functions/#sin","text":"SIN (angle) # Returns the sine of an angle provided in radians. >>> round(SIN(PI()), 10) 0.0 >>> SIN(PI()/2) 1.0 >>> round(SIN(30*PI()/180), 10) 0.5 >>> round(SIN(RADIANS(30)), 10) 0.5","title":"SIN"},{"location":"functions/#sinh","text":"SINH (value) # Returns the hyperbolic sine of any real number. >>> round(2.868*SINH(0.0342*1.03), 7) 0.1010491","title":"SINH"},{"location":"functions/#sqrt","text":"SQRT (value) # Returns the positive square root of a positive number. >>> SQRT(16) 4.0 >>> SQRT(-16) Traceback (most recent call last): ... ValueError: math domain error >>> SQRT(ABS(-16)) 4.0","title":"SQRT"},{"location":"functions/#sqrtpi","text":"SQRTPI (value) # Returns the positive square root of the product of Pi and the given positive number. >>> round(SQRTPI(1), 6) 1.772454 >>> round(SQRTPI(2), 6) 2.506628","title":"SQRTPI"},{"location":"functions/#subtotal","text":"SUBTOTAL (function_code, range1, range2) # Returns a subtotal for a vertical range of cells using a specified aggregation function. Note This function is not currently implemented in Grist.","title":"SUBTOTAL"},{"location":"functions/#sum","text":"SUM (value1, *more_values) # Returns the sum of a series of numbers. Each argument may be a number or an array. Non-numeric values are ignored. >>> SUM([5,15,30]) 50 >>> SUM([5.,15,30], 2) 52.0 >>> SUM(5,15,[30],[2]) 52","title":"SUM"},{"location":"functions/#sumif","text":"SUMIF (records, criterion, sum_range) # Returns a conditional sum across a range. Note This function is not currently implemented in Grist.","title":"SUMIF"},{"location":"functions/#sumifs","text":"SUMIFS (sum_range, criteria_range1, criterion1, *args) # Returns the sum of a range depending on multiple criteria. Note This function is not currently implemented in Grist.","title":"SUMIFS"},{"location":"functions/#sumproduct","text":"SUMPRODUCT (array1, *more_arrays) # Multiplies corresponding components in two equally-sized arrays, and returns the sum of those products. >>> SUMPRODUCT([3,8,1,4,6,9], [2,6,5,7,7,3]) 156 >>> SUMPRODUCT([], [], []) 0 >>> SUMPRODUCT([-0.25], [-2], [-3]) -1.5 >>> SUMPRODUCT([-0.25, -0.25], [-2, -2], [-3, -3]) -3.0","title":"SUMPRODUCT"},{"location":"functions/#sumsq","text":"SUMSQ (value1, value2) # Returns the sum of the squares of a series of numbers and/or cells. Note This function is not currently implemented in Grist.","title":"SUMSQ"},{"location":"functions/#tan","text":"TAN (angle) # Returns the tangent of an angle provided in radians. >>> round(TAN(0.785), 8) 0.99920399 >>> round(TAN(45*PI()/180), 10) 1.0 >>> round(TAN(RADIANS(45)), 10) 1.0","title":"TAN"},{"location":"functions/#tanh","text":"TANH (value) # Returns the hyperbolic tangent of any real number. >>> round(TANH(-2), 6) -0.964028 >>> TANH(0) 0.0 >>> round(TANH(0.5), 6) 0.462117","title":"TANH"},{"location":"functions/#trunc","text":"TRUNC (value, places=0) # Truncates a number to a certain number of significant digits by omitting less significant digits. >>> TRUNC(8.9) 8 >>> TRUNC(-8.9) -8 >>> TRUNC(0.45) 0","title":"TRUNC"},{"location":"functions/#uuid","text":"UUID () # Generate a random UUID-formatted string identifier. Since UUID() produces a different value each time it\u2019s called, it is best to use it in trigger formula for new records. This would only calculate UUID() once and freeze the calculated value. By contrast, a regular formula may get recalculated any time the document is reloaded, producing a different value for UUID() each time.","title":"UUID"},{"location":"functions/#schedule","text":"","title":"Schedule"},{"location":"functions/#schedule_1","text":"SCHEDULE (schedule, start=None, count=10, end=None) # Returns the list of datetime objects generated according to the schedule string. Starts at start , which defaults to NOW(). Generates at most count results (10 by default). If end is given, stops there. The schedule has the format \u201cINTERVAL: SLOTS, \u2026\u201d. For example: annual: Jan-15, Apr-15, Jul-15 -- Three times a year on given dates at midnight. annual: 1/15, 4/15, 7/15 -- Same as above. monthly: /1 2pm, /15 2pm -- The 1st and the 15th of each month, at 2pm. 3-months: /10, +1m /20 -- Every 3 months on the 10th of month 1, 20th of month 2. weekly: Mo 9am, Tu 9am, Fr 2pm -- Three times a week at specified times. 2-weeks: Mo, +1w Tu -- Every 2 weeks on Monday of week 1, Tuesday of week 2. daily: 07:30, 21:00 -- Twice a day at specified times. 2-day: 12am, 4pm, +1d 8am -- Three times every two days, evenly spaced. hourly: :15, :45 -- 15 minutes before and after each hour. 4-hour: :00, 1:20, 2:40 -- Three times every 4 hours, evenly spaced. 10-minute: +0s -- Every 10 minutes on the minute. INTERVAL must be either of the form N-unit where N is a number and unit is one of year , month , week , day , hour ; or one of the aliases: annual , monthly , weekly , daily , hourly , which mean 1-year , 1-month , etc. SLOTS support the following units: `Jan-15` or `1/15` -- Month and day of the month; available when INTERVAL is year-based. `/15` -- Day of the month, available when INTERVAL is month-based. `Mon`, `Mo`, `Friday` -- Day of the week (or abbreviation), when INTERVAL is week-based. 10am, 1:30pm, 15:45 -- Time of day, available for day-based or longer intervals. :45, :00 -- Minutes of the hour, available when INTERVAL is hour-based. +1d, +15d -- How many days to add to start of INTERVAL. +1w -- How many weeks to add to start of INTERVAL. +1m -- How many months to add to start of INTERVAL. The SLOTS are always relative to the INTERVAL rather than to start . Week-based intervals start on Sunday. E.g. weekly: +1d, +4d is the same as weekly: Mon, Thu , and generates times on Mondays and Thursdays regardless of start . The first generated time is determined by the unit of the INTERVAL without regard to the multiple. E.g. both \u201c2-week: Mon\u201d and \u201c3-week: Mon\u201d start on the first Monday after start , and then generate either every second or every third Monday after that. Similarly, 24-hour: :00 starts with the first top-of-the-hour after start (not with midnight), and then repeats every 24 hours. To start with the midnight after start , use daily: 0:00 . For interval units of a day or longer, if time-of-day is not specified, it defaults to midnight. The time zone of start determines the time zone of the generated times. >>> def show(dates): return [d.strftime(\"%Y-%m-%d %H:%M\") for d in dates] >>> start = datetime(2018, 9, 4, 14, 0); # 2pm on Tue, Sep 4 2018. >>> show(SCHEDULE('annual: Jan-15, Apr-15, Jul-15, Oct-15', start=start, count=4)) ['2018-10-15 00:00', '2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00'] >>> show(SCHEDULE('annual: 1/15, 4/15, 7/15', start=start, count=4)) ['2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00', '2020-01-15 00:00'] >>> show(SCHEDULE('monthly: /1 2pm, /15 5pm', start=start, count=4)) ['2018-09-15 17:00', '2018-10-01 14:00', '2018-10-15 17:00', '2018-11-01 14:00'] >>> show(SCHEDULE('3-months: /10, +1m /20', start=start, count=4)) ['2018-09-10 00:00', '2018-10-20 00:00', '2018-12-10 00:00', '2019-01-20 00:00'] >>> show(SCHEDULE('weekly: Mo 9am, Tu 9am, Fr 2pm', start=start, count=4)) ['2018-09-07 14:00', '2018-09-10 09:00', '2018-09-11 09:00', '2018-09-14 14:00'] >>> show(SCHEDULE('2-weeks: Mo, +1w Tu', start=start, count=4)) ['2018-09-11 00:00', '2018-09-17 00:00', '2018-09-25 00:00', '2018-10-01 00:00'] >>> show(SCHEDULE('daily: 07:30, 21:00', start=start, count=4)) ['2018-09-04 21:00', '2018-09-05 07:30', '2018-09-05 21:00', '2018-09-06 07:30'] >>> show(SCHEDULE('2-day: 12am, 4pm, +1d 8am', start=start, count=4)) ['2018-09-04 16:00', '2018-09-05 08:00', '2018-09-06 00:00', '2018-09-06 16:00'] >>> show(SCHEDULE('hourly: :15, :45', start=start, count=4)) ['2018-09-04 14:15', '2018-09-04 14:45', '2018-09-04 15:15', '2018-09-04 15:45'] >>> show(SCHEDULE('4-hour: :00, +1H :20, +2H :40', start=start, count=4)) ['2018-09-04 14:00', '2018-09-04 15:20', '2018-09-04 16:40', '2018-09-04 18:00']","title":"SCHEDULE"},{"location":"functions/#stats","text":"","title":"Stats"},{"location":"functions/#avedev","text":"AVEDEV (value1, value2) # Calculates the average of the magnitudes of deviations of data from a dataset\u2019s mean. Note This function is not currently implemented in Grist.","title":"AVEDEV"},{"location":"functions/#average","text":"AVERAGE (value, *more_values) # Returns the numerical average value in a dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. >>> AVERAGE([2, -1.0, 11]) 4.0 >>> AVERAGE([2, -1, 11, \"Hello\"]) 4.0 >>> AVERAGE([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11]) 4.0 >>> AVERAGE(False, True) Traceback (most recent call last): ... ZeroDivisionError: float division by zero","title":"AVERAGE"},{"location":"functions/#averagea","text":"AVERAGEA (value, *more_values) # Returns the numerical average value in a dataset, counting non-numerical values as 0. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. >>> AVERAGEA([2, -1.0, 11]) 4.0 >>> AVERAGEA([2, -1, 11, \"Hello\"]) 3.0 >>> AVERAGEA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 1.5 >>> AVERAGEA(False, True) 0.5","title":"AVERAGEA"},{"location":"functions/#averageif","text":"AVERAGEIF (criteria_range, criterion, average_range=None) # Returns the average of a range depending on criteria. Note This function is not currently implemented in Grist.","title":"AVERAGEIF"},{"location":"functions/#averageifs","text":"AVERAGEIFS (average_range, criteria_range1, criterion1, *args) # Returns the average of a range depending on multiple criteria. Note This function is not currently implemented in Grist.","title":"AVERAGEIFS"},{"location":"functions/#average_weighted","text":"AVERAGE_WEIGHTED (pairs) # Given a list of (value, weight) pairs, finds the average of the values weighted by the corresponding weights. Ignores any pairs with a non-numerical value or weight. If you have two lists, of values and weights, use the Python built-in zip() function to create a list of pairs. >>> AVERAGE_WEIGHTED(((95, .25), (90, .1), (\"X\", .5), (85, .15), (88, .2), (82, .3), (70, None))) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, \"X\", 85, 88, 82, 70], [25, 10, 50, 15, 20, 30, None])) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, False, 85, 88, 82, 70], [.25, .1, .5, .15, .2, .3, True])) 87.7","title":"AVERAGE_WEIGHTED"},{"location":"functions/#binomdist","text":"BINOMDIST (num_successes, num_trials, prob_success, cumulative) # Calculates the probability of drawing a certain number of successes (or a maximum number of successes) in a certain number of tries given a population of a certain size containing a certain number of successes, with replacement of draws. Note This function is not currently implemented in Grist.","title":"BINOMDIST"},{"location":"functions/#confidence","text":"CONFIDENCE (alpha, standard_deviation, pop_size) # Calculates the width of half the confidence interval for a normal distribution. Note This function is not currently implemented in Grist.","title":"CONFIDENCE"},{"location":"functions/#correl","text":"CORREL (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist.","title":"CORREL"},{"location":"functions/#count","text":"COUNT (value, *more_values) # Returns the count of numerical and date/datetime values in a dataset, ignoring other types of values. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. >>> COUNT([2, -1.0, 11]) 3 >>> COUNT([2, -1, 11, \"Hello\"]) 3 >>> COUNT([DATE(2000, 1, 1), DATE(2000, 1, 2), DATE(2000, 1, 3), \"Hello\"]) 3 >>> COUNT([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 4 >>> COUNT(False, True) 0","title":"COUNT"},{"location":"functions/#counta","text":"COUNTA (value, *more_values) # Returns the count of all values in a dataset, including non-numerical values. Each argument may be a value or an array. >>> COUNTA([2, -1.0, 11]) 3 >>> COUNTA([2, -1, 11, \"Hello\"]) 4 >>> COUNTA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 9 >>> COUNTA(False, True) 2","title":"COUNTA"},{"location":"functions/#covar","text":"COVAR (data_y, data_x) # Calculates the covariance of a dataset. Note This function is not currently implemented in Grist.","title":"COVAR"},{"location":"functions/#critbinom","text":"CRITBINOM (num_trials, prob_success, target_prob) # Calculates the smallest value for which the cumulative binomial distribution is greater than or equal to a specified criteria. Note This function is not currently implemented in Grist.","title":"CRITBINOM"},{"location":"functions/#devsq","text":"DEVSQ (value1, value2) # Calculates the sum of squares of deviations based on a sample. Note This function is not currently implemented in Grist.","title":"DEVSQ"},{"location":"functions/#expondist","text":"EXPONDIST (x, lambda_, cumulative) # Returns the value of the exponential distribution function with a specified lambda at a specified value. Note This function is not currently implemented in Grist.","title":"EXPONDIST"},{"location":"functions/#fdist","text":"FDIST (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist.","title":"FDIST"},{"location":"functions/#fisher","text":"FISHER (value) # Returns the Fisher transformation of a specified value. Note This function is not currently implemented in Grist.","title":"FISHER"},{"location":"functions/#fisherinv","text":"FISHERINV (value) # Returns the inverse Fisher transformation of a specified value. Note This function is not currently implemented in Grist.","title":"FISHERINV"},{"location":"functions/#forecast","text":"FORECAST (x, data_y, data_x) # Calculates the expected y-value for a specified x based on a linear regression of a dataset. Note This function is not currently implemented in Grist.","title":"FORECAST"},{"location":"functions/#f_dist","text":"F_DIST (x, degrees_freedom1, degrees_freedom2, cumulative) # Calculates the left-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist.","title":"F_DIST"},{"location":"functions/#f_dist_rt","text":"F_DIST_RT (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist.","title":"F_DIST_RT"},{"location":"functions/#geomean","text":"GEOMEAN (value1, value2) # Calculates the geometric mean of a dataset. Note This function is not currently implemented in Grist.","title":"GEOMEAN"},{"location":"functions/#harmean","text":"HARMEAN (value1, value2) # Calculates the harmonic mean of a dataset. Note This function is not currently implemented in Grist.","title":"HARMEAN"},{"location":"functions/#hypgeomdist","text":"HYPGEOMDIST (num_successes, num_draws, successes_in_pop, pop_size) # Calculates the probability of drawing a certain number of successes in a certain number of tries given a population of a certain size containing a certain number of successes, without replacement of draws. Note This function is not currently implemented in Grist.","title":"HYPGEOMDIST"},{"location":"functions/#intercept","text":"INTERCEPT (data_y, data_x) # Calculates the y-value at which the line resulting from linear regression of a dataset will intersect the y-axis (x=0). Note This function is not currently implemented in Grist.","title":"INTERCEPT"},{"location":"functions/#kurt","text":"KURT (value1, value2) # Calculates the kurtosis of a dataset, which describes the shape, and in particular the \u201cpeakedness\u201d of that dataset. Note This function is not currently implemented in Grist.","title":"KURT"},{"location":"functions/#large","text":"LARGE (data, n) # Returns the nth largest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist.","title":"LARGE"},{"location":"functions/#loginv","text":"LOGINV (x, mean, standard_deviation) # Returns the value of the inverse log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist.","title":"LOGINV"},{"location":"functions/#lognormdist","text":"LOGNORMDIST (x, mean, standard_deviation) # Returns the value of the log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist.","title":"LOGNORMDIST"},{"location":"functions/#max","text":"MAX (value, *more_values) # Returns the maximum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MAX([2, -1.5, 11.5]) 11.5 >>> MAX([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAX(True, -123) -123 >>> MAX(\"123\", -123) -123 >>> MAX(\"Hello\", \"123\", True, False) 0 >>> MAX(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 2) >>> MAX(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56) >>> MAX(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 2)","title":"MAX"},{"location":"functions/#maxa","text":"MAXA (value, *more_values) # Returns the maximum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MAXA([2, -1.5, 11.5]) 11.5 >>> MAXA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAXA(True, -123) 1 >>> MAXA(\"123\", -123) 0 >>> MAXA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0","title":"MAXA"},{"location":"functions/#median","text":"MEDIAN (value, *more_values) # Returns the median value in a numeric dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. Produces an error if the arguments contain no numbers. The median is the middle number when all values are sorted. So half of the values in the dataset are less than the median, and half of the values are greater. If there is an even number of values in the dataset, returns the average of the two numbers in the middle. >>> MEDIAN(1, 2, 3, 4, 5) 3 >>> MEDIAN(3, 5, 1, 4, 2) 3 >>> MEDIAN(range(10)) 4.5 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1), 12.3) 12.3 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1)) Traceback (most recent call last): ... ValueError: MEDIAN requires at least one number","title":"MEDIAN"},{"location":"functions/#min","text":"MIN (value, *more_values) # Returns the minimum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MIN([2, -1.5, 11.5]) -1.5 >>> MIN([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MIN(True, 123) 123 >>> MIN(\"-123\", 123) 123 >>> MIN(\"Hello\", \"123\", True, False) 0 >>> MIN(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56)","title":"MIN"},{"location":"functions/#mina","text":"MINA (value, *more_values) # Returns the minimum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MINA([2, -1.5, 11.5]) -1.5 >>> MINA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MINA(True, 123) 1 >>> MINA(\"-123\", 123) 0 >>> MINA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0","title":"MINA"},{"location":"functions/#mode","text":"MODE (value1, value2) # Returns the most commonly occurring value in a dataset. Note This function is not currently implemented in Grist.","title":"MODE"},{"location":"functions/#negbinomdist","text":"NEGBINOMDIST (num_failures, num_successes, prob_success) # Calculates the probability of drawing a certain number of failures before a certain number of successes given a probability of success in independent trials. Note This function is not currently implemented in Grist.","title":"NEGBINOMDIST"},{"location":"functions/#normdist","text":"NORMDIST (x, mean, standard_deviation, cumulative) # Returns the value of the normal distribution function (or normal cumulative distribution function) for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist.","title":"NORMDIST"},{"location":"functions/#norminv","text":"NORMINV (x, mean, standard_deviation) # Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist.","title":"NORMINV"},{"location":"functions/#normsdist","text":"NORMSDIST (x) # Returns the value of the standard normal cumulative distribution function for a specified value. Note This function is not currently implemented in Grist.","title":"NORMSDIST"},{"location":"functions/#normsinv","text":"NORMSINV (x) # Returns the value of the inverse standard normal distribution function for a specified value. Note This function is not currently implemented in Grist.","title":"NORMSINV"},{"location":"functions/#pearson","text":"PEARSON (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist.","title":"PEARSON"},{"location":"functions/#percentile","text":"PERCENTILE (data, percentile) # Returns the value at a given percentile of a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTILE"},{"location":"functions/#percentrank","text":"PERCENTRANK (data, value, significant_digits=None) # Returns the percentage rank (percentile) of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTRANK"},{"location":"functions/#percentrank_exc","text":"PERCENTRANK_EXC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 exclusive of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTRANK_EXC"},{"location":"functions/#percentrank_inc","text":"PERCENTRANK_INC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 inclusive of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTRANK_INC"},{"location":"functions/#permut","text":"PERMUT (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects, considering order. Note This function is not currently implemented in Grist.","title":"PERMUT"},{"location":"functions/#poisson","text":"POISSON (x, mean, cumulative) # Returns the value of the Poisson distribution function (or Poisson cumulative distribution function) for a specified value and mean. Note This function is not currently implemented in Grist.","title":"POISSON"},{"location":"functions/#prob","text":"PROB (data, probabilities, low_limit, high_limit=None) # Given a set of values and corresponding probabilities, calculates the probability that a value chosen at random falls between two limits. Note This function is not currently implemented in Grist.","title":"PROB"},{"location":"functions/#quartile","text":"QUARTILE (data, quartile_number) # Returns a value nearest to a specified quartile of a dataset. Note This function is not currently implemented in Grist.","title":"QUARTILE"},{"location":"functions/#rank","text":"RANK (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"RANK"},{"location":"functions/#rank_avg","text":"RANK_AVG (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the average rank of the entries will be returned. Note This function is not currently implemented in Grist.","title":"RANK_AVG"},{"location":"functions/#rank_eq","text":"RANK_EQ (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the top rank of the entries will be returned. Note This function is not currently implemented in Grist.","title":"RANK_EQ"},{"location":"functions/#rsq","text":"RSQ (data_y, data_x) # Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist.","title":"RSQ"},{"location":"functions/#skew","text":"SKEW (value1, value2) # Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean. Note This function is not currently implemented in Grist.","title":"SKEW"},{"location":"functions/#slope","text":"SLOPE (data_y, data_x) # Calculates the slope of the line resulting from linear regression of a dataset. Note This function is not currently implemented in Grist.","title":"SLOPE"},{"location":"functions/#small","text":"SMALL (data, n) # Returns the nth smallest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist.","title":"SMALL"},{"location":"functions/#standardize","text":"STANDARDIZE (value, mean, standard_deviation) # Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution. Note This function is not currently implemented in Grist.","title":"STANDARDIZE"},{"location":"functions/#stdev","text":"STDEV (value, *more_values) # Calculates the standard deviation based on a sample, ignoring non-numerical values. >>> STDEV([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10], 3, 12, 15) 4.810702354423639 >>> STDEV([2, 5, 8, 13, 10], [3, 12, 15]) 4.810702354423639 >>> STDEV([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero","title":"STDEV"},{"location":"functions/#stdeva","text":"STDEVA (value, *more_values) # Calculates the standard deviation based on a sample, setting text to the value 0 . >>> STDEVA([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEVA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], 1, 0, 0) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], [1, 0, 0]) 4.969550137731641 >>> STDEVA([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero","title":"STDEVA"},{"location":"functions/#stdevp","text":"STDEVP (value, *more_values) # Calculates the standard deviation based on an entire population, ignoring non-numerical values. >>> STDEVP([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10, True, False, \"Test\"]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10], 3, 12, 15) 4.5 >>> STDEVP([2, 5, 8, 13, 10], [3, 12, 15]) 4.5 >>> STDEVP([5]) 0.0","title":"STDEVP"},{"location":"functions/#stdevpa","text":"STDEVPA (value, *more_values) # Calculates the standard deviation based on an entire population, setting text to the value 0 . >>> STDEVPA([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVPA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], 1, 0, 0) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], [1, 0, 0]) 4.648588495446763 >>> STDEVPA([5]) 0.0","title":"STDEVPA"},{"location":"functions/#steyx","text":"STEYX (data_y, data_x) # Calculates the standard error of the predicted y-value for each x in the regression of a dataset. Note This function is not currently implemented in Grist.","title":"STEYX"},{"location":"functions/#tdist","text":"TDIST (x, degrees_freedom, tails) # Calculates the probability for Student\u2019s t-distribution with a given input (x). Note This function is not currently implemented in Grist.","title":"TDIST"},{"location":"functions/#tinv","text":"TINV (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist.","title":"TINV"},{"location":"functions/#trimmean","text":"TRIMMEAN (data, exclude_proportion) # Calculates the mean of a dataset excluding some proportion of data from the high and low ends of the dataset. Note This function is not currently implemented in Grist.","title":"TRIMMEAN"},{"location":"functions/#ttest","text":"TTEST (range1, range2, tails, type) # Returns the probability associated with t-test. Determines whether two samples are likely to have come from the same two underlying populations that have the same mean. Note This function is not currently implemented in Grist.","title":"TTEST"},{"location":"functions/#t_inv","text":"T_INV (probability, degrees_freedom) # Calculates the negative inverse of the one-tailed TDIST function. Note This function is not currently implemented in Grist.","title":"T_INV"},{"location":"functions/#t_inv_2t","text":"T_INV_2T (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist.","title":"T_INV_2T"},{"location":"functions/#var","text":"VAR (value1, value2) # Calculates the variance based on a sample. Note This function is not currently implemented in Grist.","title":"VAR"},{"location":"functions/#vara","text":"VARA (value1, value2) # Calculates an estimate of variance based on a sample, setting text to the value 0 . Note This function is not currently implemented in Grist.","title":"VARA"},{"location":"functions/#varp","text":"VARP (value1, value2) # Calculates the variance based on an entire population. Note This function is not currently implemented in Grist.","title":"VARP"},{"location":"functions/#varpa","text":"VARPA (value1, value2) # Calculates the variance based on an entire population, setting text to the value 0 . Note This function is not currently implemented in Grist.","title":"VARPA"},{"location":"functions/#weibull","text":"WEIBULL (x, shape, scale, cumulative) # Returns the value of the Weibull distribution function (or Weibull cumulative distribution function) for a specified shape and scale. Note This function is not currently implemented in Grist.","title":"WEIBULL"},{"location":"functions/#ztest","text":"ZTEST (data, value, standard_deviation) # Returns the two-tailed P-value of a Z-test with standard distribution. Note This function is not currently implemented in Grist.","title":"ZTEST"},{"location":"functions/#text","text":"","title":"Text"},{"location":"functions/#char","text":"CHAR (table_number) # Convert a number into a character according to the current Unicode table. Same as unichr(number) . >>> CHAR(65) u'A' >>> CHAR(33) u'!'","title":"CHAR"},{"location":"functions/#clean","text":"CLEAN (text) # Returns the text with the non-printable characters removed. This removes both characters with values 0 through 31, and other Unicode characters in the \u201ccontrol characters\u201d category. >>> CLEAN(CHAR(9) + \"Monthly report\" + CHAR(10)) u'Monthly report'","title":"CLEAN"},{"location":"functions/#code","text":"CODE (string) # Returns the numeric Unicode map value of the first character in the string provided. Same as ord(string[0]) . >>> CODE(\"A\") 65 >>> CODE(\"!\") 33 >>> CODE(\"!A\") 33","title":"CODE"},{"location":"functions/#concat","text":"CONCAT (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCATENATE . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCAT(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCAT(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCAT(\"abc\") u'abc' >>> CONCAT(0, \"abc\") u'0abc' >>> assert CONCAT(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e'","title":"CONCAT"},{"location":"functions/#concatenate","text":"CONCATENATE (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCAT . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCATENATE(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCATENATE(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCATENATE(\"abc\") u'abc' >>> CONCATENATE(0, \"abc\") u'0abc' >>> assert CONCATENATE(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", \"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e'","title":"CONCATENATE"},{"location":"functions/#dollar","text":"DOLLAR (number, decimals=2) # Formats a number into a formatted dollar amount, with decimals rounded to the specified place (. If decimals value is omitted, it defaults to 2. >>> DOLLAR(1234.567) '$1,234.57' >>> DOLLAR(1234.567, -2) '$1,200' >>> DOLLAR(-1234.567, -2) '($1,200)' >>> DOLLAR(-0.123, 4) '($0.1230)' >>> DOLLAR(99.888) '$99.89' >>> DOLLAR(0) '$0.00' >>> DOLLAR(10, 0) '$10'","title":"DOLLAR"},{"location":"functions/#exact","text":"EXACT (string1, string2) # Tests whether two strings are identical. Same as string2 == string2 . >>> EXACT(\"word\", \"word\") True >>> EXACT(\"Word\", \"word\") False >>> EXACT(\"w ord\", \"word\") False","title":"EXACT"},{"location":"functions/#find","text":"FIND (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> FIND(\"M\", \"Miriam McGovern\") 1 >>> FIND(\"m\", \"Miriam McGovern\") 6 >>> FIND(\"M\", \"Miriam McGovern\", 3) 8 >>> FIND(\" #\", \"Hello world # Test\") 12 >>> FIND(\"gle\", \"Google\", 1) 4 >>> FIND(\"GLE\", \"Google\", 1) Traceback (most recent call last): ... ValueError: substring not found >>> FIND(\"page\", \"homepage\") 5 >>> FIND(\"page\", \"homepage\", 6) Traceback (most recent call last): ... ValueError: substring not found","title":"FIND"},{"location":"functions/#fixed","text":"FIXED (number, decimals=2, no_commas=False) # Formats a number with a fixed number of decimal places (2 by default), and commas. If no_commas is True, then omits the commas. >>> FIXED(1234.567, 1) '1,234.6' >>> FIXED(1234.567, -1) '1,230' >>> FIXED(-1234.567, -1, True) '-1230' >>> FIXED(44.332) '44.33' >>> FIXED(3521.478, 2, False) '3,521.48' >>> FIXED(-3521.478, 1, True) '-3521.5' >>> FIXED(3521.478, 0, True) '3521' >>> FIXED(3521.478, -2, True) '3500'","title":"FIXED"},{"location":"functions/#left","text":"LEFT (string, num_chars=1) # Returns a substring of length num_chars from the beginning of the given string. If num_chars is omitted, it is assumed to be 1. Same as string[:num_chars] . >>> LEFT(\"Sale Price\", 4) 'Sale' >>> LEFT('Swededn') 'S' >>> LEFT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid","title":"LEFT"},{"location":"functions/#len","text":"LEN (text) # Returns the number of characters in a text string, or the number of items in a list. Same as len in python. See Record Set for an example of using len on a list of records. >>> LEN(\"Phoenix, AZ\") 11 >>> LEN(\"\") 0 >>> LEN(\" One \") 11","title":"LEN"},{"location":"functions/#lower","text":"LOWER (text) # Converts a specified string to lowercase. Same as text.lower() . >>> LOWER(\"E. E. Cummings\") 'e. e. cummings' >>> LOWER(\"Apt. 2B\") 'apt. 2b'","title":"LOWER"},{"location":"functions/#mid","text":"MID (text, start_num, num_chars) # Returns a segment of a string, starting at start_num. The first character in text has start_num 1. >>> MID(\"Fluid Flow\", 1, 5) 'Fluid' >>> MID(\"Fluid Flow\", 7, 20) 'Flow' >>> MID(\"Fluid Flow\", 20, 5) '' >>> MID(\"Fluid Flow\", 0, 5) Traceback (most recent call last): ... ValueError: start_num invalid","title":"MID"},{"location":"functions/#phone_format","text":"PHONE_FORMAT (value, country=None, format=None) # Formats a phone number. With no optional arguments, the number must start with \u201c+\u201d and the international dialing prefix, and will be formatted as an international number, e.g. +12345678901 becomes +1 234-567-8901 . The country argument allows specifying a 2-letter country code (e.g. \u201cUS\u201d or \u201cGB\u201d) for interpreting phone numbers that don\u2019t start with \u201c+\u201d. E.g. PHONE_FORMAT('2025555555', 'US') would be seen as a US number and formatted as \u201c(202) 555-5555\u201d. Phone numbers that start with \u201c+\u201d ignore country . E.g. PHONE_FORMAT('+33555555555', 'US') is a French number because \u2018+33\u2019 is the international prefix for France. The format argument specifies the output format, according to this table: \"#\" or \"NATL\" (default) - use the national format, without the international dialing prefix, when possible. E.g. (234) 567-8901 for \u201cUS\u201d, or 02 34 56 78 90 for \u201cFR\u201d. If country is omitted, or the number does not correspond to the given country, the international format is used instead. \"+\" or \"INTL\" - international format, e.g. +1 234-567-8901 or +33 2 34 56 78 90 . \"*\" or \"E164\" - E164 format, like international but with no separators, e.g. +12345678901 . \"tel\" or \"RFC3966\" - format suitable to use as a hyperlink , e.g. \u2018tel:+1-234-567-8901\u2019. When specifying the format argument, you may omit the country argument. I.e. PHONE_FORMAT(value, \"tel\") is equivalent to PHONE_FORMAT(value, None, \"tel\") . For more details, see the phonenumbers Python library, which underlies this function. >>> PHONE_FORMAT(\"+12345678901\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"2345678901\", \"US\") u'(234) 567-8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\", \"+\") u'+44 23 4567 8901' >>> PHONE_FORMAT(\"+442345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"+12345678901\", \"GB\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"(234) 567-8901\") Traceback (most recent call last): ... NumberParseException: (0) Missing or invalid default region. >>> PHONE_FORMAT(\"(234)567 89-01\", \"US\", \"tel\") u'tel:+1-234-567-8901' >>> PHONE_FORMAT(\"2/3456/7890\", \"FR\", '#') u'02 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", '#') u'+33 2 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", 'tel') u'tel:+33-2-34-56-78-90' >>> PHONE_FORMAT(\"tel:+1-234-567-8901\", country=\"US\", format=\"*\") u'+12345678901'","title":"PHONE_FORMAT"},{"location":"functions/#proper","text":"PROPER (text) # Capitalizes each word in a specified string. It converts the first letter of each word to uppercase, and all other letters to lowercase. Same as text.title() . >>> PROPER('this is a TITLE') 'This Is A Title' >>> PROPER('2-way street') '2-Way Street' >>> PROPER('76BudGet') '76Budget'","title":"PROPER"},{"location":"functions/#regexextract","text":"REGEXEXTRACT (text, regular_expression) # Extracts the first part of text that matches regular_expression. >>> REGEXEXTRACT(\"Google Doc 101\", \"[0-9]+\") '101' >>> REGEXEXTRACT(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") '826.25' If there is a parenthesized expression, it is returned instead of the whole match. >>> REGEXEXTRACT(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") 'Content' >>> REGEXEXTRACT(\"Foo\", \"Bar\") Traceback (most recent call last): ... ValueError: REGEXEXTRACT text does not match","title":"REGEXEXTRACT"},{"location":"functions/#regexmatch","text":"REGEXMATCH (text, regular_expression) # Returns whether a piece of text matches a regular expression. >>> REGEXMATCH(\"Google Doc 101\", \"[0-9]+\") True >>> REGEXMATCH(\"Google Doc\", \"[0-9]+\") False >>> REGEXMATCH(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") True >>> REGEXMATCH(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") True >>> REGEXMATCH(\"Foo\", \"Bar\") False","title":"REGEXMATCH"},{"location":"functions/#regexreplace","text":"REGEXREPLACE (text, regular_expression, replacement) # Replaces all parts of text matching the given regular expression with replacement text. >>> REGEXREPLACE(\"Google Doc 101\", \"[0-9]+\", \"777\") 'Google Doc 777' >>> REGEXREPLACE(\"Google Doc\", \"[0-9]+\", \"777\") 'Google Doc' >>> REGEXREPLACE(\"The price is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\", \"315.75\") 'The price is $315.75' >>> REGEXREPLACE(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\", \"Word\") 'Word between brackets' >>> REGEXREPLACE(\"Foo\", \"Bar\", \"Baz\") 'Foo'","title":"REGEXREPLACE"},{"location":"functions/#replace","text":"REPLACE (text, position, length, new_text) # Replaces part of a text string with a different text string. Position is counted from 1. >>> REPLACE(\"abcdefghijk\", 6, 5, \"*\") 'abcde*k' >>> REPLACE(\"2009\", 3, 2, \"10\") '2010' >>> REPLACE('123456', 1, 3, '@') '@456' >>> REPLACE('foo', 1, 0, 'bar') 'barfoo' >>> REPLACE('foo', 0, 1, 'bar') Traceback (most recent call last): ... ValueError: position invalid","title":"REPLACE"},{"location":"functions/#rept","text":"REPT (text, number_times) # Returns specified text repeated a number of times. Same as text * number_times . The result of the REPT function cannot be longer than 32767 characters, or it raises a ValueError. >>> REPT(\"*-\", 3) '*-*-*-' >>> REPT('-', 10) '----------' >>> REPT('-', 0) '' >>> len(REPT('---', 10000)) 30000 >>> REPT('---', 11000) Traceback (most recent call last): ... ValueError: number_times invalid >>> REPT('-', -1) Traceback (most recent call last): ... ValueError: number_times invalid","title":"REPT"},{"location":"functions/#right","text":"RIGHT (string, num_chars=1) # Returns a substring of length num_chars from the end of a specified string. If num_chars is omitted, it is assumed to be 1. Same as string[-num_chars:] . >>> RIGHT(\"Sale Price\", 5) 'Price' >>> RIGHT('Stock Number') 'r' >>> RIGHT('Text', 100) 'Text' >>> RIGHT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid","title":"RIGHT"},{"location":"functions/#search","text":"SEARCH (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text, ignoring case. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> SEARCH(\"e\", \"Statements\", 6) 7 >>> SEARCH(\"margin\", \"Profit Margin\") 8 >>> SEARCH(\" \", \"Profit Margin\") 7 >>> SEARCH('\"', 'The \"boss\" is here.') 5 >>> SEARCH(\"gle\", \"Google\") 4 >>> SEARCH(\"GLE\", \"Google\") 4","title":"SEARCH"},{"location":"functions/#substitute","text":"SUBSTITUTE (text, old_text, new_text, instance_num=None) # Replaces existing text with new text in a string. It is useful when you know the substring of text to replace. Use REPLACE when you know the position of text to replace. If instance_num is given, it specifies which occurrence of old_text to replace. If omitted, all occurrences are replaced. Same as text.replace(old_text, new_text) when instance_num is omitted. >>> SUBSTITUTE(\"Sales Data\", \"Sales\", \"Cost\") u'Cost Data' >>> SUBSTITUTE(\"Quarter 1, 2008\", \"1\", \"2\", 1) u'Quarter 2, 2008' >>> SUBSTITUTE(\"Quarter 1, 2011\", \"1\", \"2\", 3) u'Quarter 1, 2012'","title":"SUBSTITUTE"},{"location":"functions/#t","text":"T (value) # Returns value if value is text, or the empty string when value is not text. >>> T('Text') u'Text' >>> T(826) u'' >>> T('826') u'826' >>> T(False) u'' >>> T('100 points') u'100 points' >>> T(AltText('Text')) u'Text' >>> T(float('nan')) u''","title":"T"},{"location":"functions/#text_1","text":"TEXT (number, format_type) # Converts a number into text according to a specified format. It is not yet implemented in Grist. Note This function is not currently implemented in Grist.","title":"TEXT"},{"location":"functions/#trim","text":"TRIM (text) # Removes all spaces from text except for single spaces between words. Note that TRIM does not remove other whitespace such as tab or newline characters. >>> TRIM(\" First Quarter\\n Earnings \") 'First Quarter\\n Earnings' >>> TRIM(\"\") ''","title":"TRIM"},{"location":"functions/#upper","text":"UPPER (text) # Converts a specified string to uppercase. Same as text.upper() . >>> UPPER(\"e. e. cummings\") 'E. E. CUMMINGS' >>> UPPER(\"Apt. 2B\") 'APT. 2B'","title":"UPPER"},{"location":"functions/#value","text":"VALUE (text) # Converts a string in accepted date, time or number formats into a number or date. >>> VALUE(\"$1,000\") 1000 >>> assert VALUE(\"16:48:00\") - VALUE(\"12:00:00\") == datetime.timedelta(0, 17280) >>> VALUE(\"01/01/2012\") datetime.datetime(2012, 1, 1, 0, 0) >>> VALUE(\"\") 0 >>> VALUE(0) 0 >>> VALUE(\"826\") 826 >>> VALUE(\"-826.123123123\") -826.123123123 >>> VALUE(float('nan')) nan >>> VALUE(\"Invalid\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number >>> VALUE(\"13/13/13\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number","title":"VALUE"},{"location":"glossary/","text":"Glossary # Bar Chart # This is a classic chart type , where the values in a column are shown as the heights of a series of rectangles. Column # A column is a vertical series of cells in a table. Columns in Grist have names. Each cell in a column is in a different row. When data from a column is present within a card, we call it a field. When a table of data is represented as a chart, we refer to each column as a series. From a data modeling perspective, columns typically have data about a single aspect of many real world entities, whereas rows have data about many aspects of a single entity. Column Options # Every column\u2019s appearance and behavior can be customized by clicking on the column header, clicking on the drop-down, and selecting \u201cColumn Options\u201d. Column Type # Columns have types, which control the appearance of cells in that column and the method used to edit them. You can change the column type at will. The Text Column Type is suited to strings of any length; the Date Column Type is specialized for storing and editing calendar dates; the Reference Column Type is for storing and editing links to other tables; the Numeric Column Type is for any kind of number; etc. Creator Panel # The creator panel is the right-side menu of configuration options for widgets and columns. Dashboard # A dashboard is just another name for a page, typically organized to give a summary or overview of a document\u2019s data. Grist is well suited to constructing dashboards, by creating pages with suitably linked widgets . Data Table # Data is stored in tables. Tables have named columns , and a sequence of rows containing values for those columns. Every row has a numeric id (available as $id in formulas) that is unique within that table. The raw data page lists all data tables in your document. Document # A Grist document is a collection of related data. If you work with databases, think of it as a single database. If you work with spreadsheets, think of it as a single spreadsheet. Like databases and spreadsheets, the data in a Grist document is organized as a set of tables. How this data is viewed and edited is unusually flexible. Grist documents are organized visually into pages. Pages contain widgets that offer different ways to view and edit tables. To work with Grist, the first step is typically to create a document . Drag handle # This is an icon to facilitate reorganizing views or lists visually. On a desktop computer, when hovering over a drag handle, the mouse cursor changes. The drag handle for a widget is just to the left of the widget\u2019s title. There is an example of using this drag handle in the investment research demo . Fiddle mode # Fiddle mode is a special mode that some Grist documents, such as the ones from the Examples & Templates page , will open in. A document opened in fiddle mode will have a \u201cfiddle\u201d label next to the document title in the top bar. In fiddle mode, any edit to a document will cause a new, unsaved copy (a.k.a \u201cfork\u201d) of that document to be created; the original document will remain unaffected. The copy can be saved via the \u201cSave Copy\u201d button or menu option. You can add /m/fork to the end of any document\u2019s URL to make that document open in fiddle mode (e.g. https://public.getgrist.com/3NsoHE2mWtEp/Lead-list/m/fork ). Field # A field is a column shown in a Card Widget. The terms column, field, and series are not different in substance, but are different terms that make more sense for different widgets. In a Table Widget, we talk about columns. In a Chart Widget, we talk about series. And in a Card Widget, we talk about fields. A field has layout properties that are meaningful within a Card, but would not be meaningful in other contexts. Import # To import into Grist means to take data from other sources (on your computer or on the internet) and place that data in a Grist document. Examples of importing include: Take a CSV file on your computer, and create a Grist document with the same content (see: start a new Grist document ). Take an Excel file on your computer, and add the data from it to an existing Grist document (see: importing more data ). Take a JSON file on the internet, and add the data from it to an existing Grist document (see: importing more data ). Calling Grist\u2019s API from a program and adding data read from another source (see: Grist API ). Lookups # Lookup formulas allow you to \u201clook up\u201d data in other tables. lookupOne allows you to look up a single record in another table by matching some data across two tables, similar to Excel\u2019s VLOOKUP. lookupRecords allows you to look up multiple records in another table by matching some data across two tables. Lookups can be combined with dot notation to pull data from referenced records. Learn how. Page # Grist documents are organized visually into pages. Each page allows you to view or edit one or more tables. The nature of these viewers and editors (called \u201cpage widgets\u201d) is flexible, as is their layout. A single table can be viewed (or edited) from multiple widgets in one or many pages. And a single page can contain widgets for viewing (or editing) many tables. Pages are listed in the document ( in the panel on the left ). In this list, they may be rearranged and grouped, with several \u201csubpages\u201d nested within a single page. Pie Chart # This is a classic chart type , where a circle is sliced up according to values in a column. Record # A record is the data in one row of a table, comprising the data in the individual cells of that row. It has a unique identifier, usually hidden but available in formulas as id . In a Card Widget or a Card List Widget, a record is represented by a single card. Row # A horizontal series of cells in a table. Each cell in a row belongs to a different column. The data stored in a row is also called a record. Typically rows have data about different aspects a single entity, whereas columns have data about a single aspect of many entities. Series # Data from a single column shown in a Chart Widget is called a series. The same data in a Card Widget is called a field, and in a Table Widget is called a column. Sort # The order in which rows of a table are shown is called the sort order. An example of changing the sort order of a table is given in the CRM tutorial . Trigger Formulas # A trigger formula is a formula that recalculates your data based on a set of conditions that you decide. They also allow you to clean data when a new value is entered ( watch webinar ), provide a sensible default value for a column or create Time and Authorship stamps . User Menu # The user menu is the menu that opens by clicking your profile icon in the top-right of Grist. From there you can manage your profile, add additional Grist accounts that you own, and see a list of team sites to which you have access. Depending on where you are, the user menu will contain additional options. For example, from a personal site you\u2019ll see the option to upgrade your plan to a team site . From a team site, depending on your role and permissions, you may be able to manage billing or edit team members . From a document, you\u2019ll find an option to edit document settings. Widget # A page contains sections, such as table grids or charts, which we call \u201cpage widgets\u201d. They are used for viewing or editing data in tables. Types of page widgets include Card Widgets , Chart Widgets , as well as the classic spreadsheet-style table grid (called a Table Widget ). Widget Options # Every page widget can have its appearance and behavior customized. How this is done varies between widget, but can always be done systematically by clicking on the three-dot menu on the top right of a widget and selecting \u201cWidget options\u201d. Wrap Text # Normally, content that doesn\u2019t fit in the width of a cell is truncated, with \u201c\u2026\u201d indicating that part of the data is hidden. When \u201cWrap Text\u201d setting is enabled, long lines will wrap, and the cell will get taller to include all content. An example of wrapping is given in the CRM tutorial .","title":"Glossary"},{"location":"glossary/#glossary","text":"","title":"Glossary"},{"location":"glossary/#bar-chart","text":"This is a classic chart type , where the values in a column are shown as the heights of a series of rectangles.","title":"Bar Chart"},{"location":"glossary/#column","text":"A column is a vertical series of cells in a table. Columns in Grist have names. Each cell in a column is in a different row. When data from a column is present within a card, we call it a field. When a table of data is represented as a chart, we refer to each column as a series. From a data modeling perspective, columns typically have data about a single aspect of many real world entities, whereas rows have data about many aspects of a single entity.","title":"Column"},{"location":"glossary/#column-options","text":"Every column\u2019s appearance and behavior can be customized by clicking on the column header, clicking on the drop-down, and selecting \u201cColumn Options\u201d.","title":"Column Options"},{"location":"glossary/#column-type","text":"Columns have types, which control the appearance of cells in that column and the method used to edit them. You can change the column type at will. The Text Column Type is suited to strings of any length; the Date Column Type is specialized for storing and editing calendar dates; the Reference Column Type is for storing and editing links to other tables; the Numeric Column Type is for any kind of number; etc.","title":"Column Type"},{"location":"glossary/#creator-panel","text":"The creator panel is the right-side menu of configuration options for widgets and columns.","title":"Creator Panel"},{"location":"glossary/#dashboard","text":"A dashboard is just another name for a page, typically organized to give a summary or overview of a document\u2019s data. Grist is well suited to constructing dashboards, by creating pages with suitably linked widgets .","title":"Dashboard"},{"location":"glossary/#data-table","text":"Data is stored in tables. Tables have named columns , and a sequence of rows containing values for those columns. Every row has a numeric id (available as $id in formulas) that is unique within that table. The raw data page lists all data tables in your document.","title":"Data Table"},{"location":"glossary/#document","text":"A Grist document is a collection of related data. If you work with databases, think of it as a single database. If you work with spreadsheets, think of it as a single spreadsheet. Like databases and spreadsheets, the data in a Grist document is organized as a set of tables. How this data is viewed and edited is unusually flexible. Grist documents are organized visually into pages. Pages contain widgets that offer different ways to view and edit tables. To work with Grist, the first step is typically to create a document .","title":"Document"},{"location":"glossary/#drag-handle","text":"This is an icon to facilitate reorganizing views or lists visually. On a desktop computer, when hovering over a drag handle, the mouse cursor changes. The drag handle for a widget is just to the left of the widget\u2019s title. There is an example of using this drag handle in the investment research demo .","title":"Drag handle"},{"location":"glossary/#fiddle-mode","text":"Fiddle mode is a special mode that some Grist documents, such as the ones from the Examples & Templates page , will open in. A document opened in fiddle mode will have a \u201cfiddle\u201d label next to the document title in the top bar. In fiddle mode, any edit to a document will cause a new, unsaved copy (a.k.a \u201cfork\u201d) of that document to be created; the original document will remain unaffected. The copy can be saved via the \u201cSave Copy\u201d button or menu option. You can add /m/fork to the end of any document\u2019s URL to make that document open in fiddle mode (e.g. https://public.getgrist.com/3NsoHE2mWtEp/Lead-list/m/fork ).","title":"Fiddle mode"},{"location":"glossary/#field","text":"A field is a column shown in a Card Widget. The terms column, field, and series are not different in substance, but are different terms that make more sense for different widgets. In a Table Widget, we talk about columns. In a Chart Widget, we talk about series. And in a Card Widget, we talk about fields. A field has layout properties that are meaningful within a Card, but would not be meaningful in other contexts.","title":"Field"},{"location":"glossary/#import","text":"To import into Grist means to take data from other sources (on your computer or on the internet) and place that data in a Grist document. Examples of importing include: Take a CSV file on your computer, and create a Grist document with the same content (see: start a new Grist document ). Take an Excel file on your computer, and add the data from it to an existing Grist document (see: importing more data ). Take a JSON file on the internet, and add the data from it to an existing Grist document (see: importing more data ). Calling Grist\u2019s API from a program and adding data read from another source (see: Grist API ).","title":"Import"},{"location":"glossary/#lookups","text":"Lookup formulas allow you to \u201clook up\u201d data in other tables. lookupOne allows you to look up a single record in another table by matching some data across two tables, similar to Excel\u2019s VLOOKUP. lookupRecords allows you to look up multiple records in another table by matching some data across two tables. Lookups can be combined with dot notation to pull data from referenced records. Learn how.","title":"Lookups"},{"location":"glossary/#page","text":"Grist documents are organized visually into pages. Each page allows you to view or edit one or more tables. The nature of these viewers and editors (called \u201cpage widgets\u201d) is flexible, as is their layout. A single table can be viewed (or edited) from multiple widgets in one or many pages. And a single page can contain widgets for viewing (or editing) many tables. Pages are listed in the document ( in the panel on the left ). In this list, they may be rearranged and grouped, with several \u201csubpages\u201d nested within a single page.","title":"Page"},{"location":"glossary/#pie-chart","text":"This is a classic chart type , where a circle is sliced up according to values in a column.","title":"Pie Chart"},{"location":"glossary/#record","text":"A record is the data in one row of a table, comprising the data in the individual cells of that row. It has a unique identifier, usually hidden but available in formulas as id . In a Card Widget or a Card List Widget, a record is represented by a single card.","title":"Record"},{"location":"glossary/#row","text":"A horizontal series of cells in a table. Each cell in a row belongs to a different column. The data stored in a row is also called a record. Typically rows have data about different aspects a single entity, whereas columns have data about a single aspect of many entities.","title":"Row"},{"location":"glossary/#series","text":"Data from a single column shown in a Chart Widget is called a series. The same data in a Card Widget is called a field, and in a Table Widget is called a column.","title":"Series"},{"location":"glossary/#sort","text":"The order in which rows of a table are shown is called the sort order. An example of changing the sort order of a table is given in the CRM tutorial .","title":"Sort"},{"location":"glossary/#trigger-formulas","text":"A trigger formula is a formula that recalculates your data based on a set of conditions that you decide. They also allow you to clean data when a new value is entered ( watch webinar ), provide a sensible default value for a column or create Time and Authorship stamps .","title":"Trigger Formulas"},{"location":"glossary/#user-menu","text":"The user menu is the menu that opens by clicking your profile icon in the top-right of Grist. From there you can manage your profile, add additional Grist accounts that you own, and see a list of team sites to which you have access. Depending on where you are, the user menu will contain additional options. For example, from a personal site you\u2019ll see the option to upgrade your plan to a team site . From a team site, depending on your role and permissions, you may be able to manage billing or edit team members . From a document, you\u2019ll find an option to edit document settings.","title":"User Menu"},{"location":"glossary/#widget","text":"A page contains sections, such as table grids or charts, which we call \u201cpage widgets\u201d. They are used for viewing or editing data in tables. Types of page widgets include Card Widgets , Chart Widgets , as well as the classic spreadsheet-style table grid (called a Table Widget ).","title":"Widget"},{"location":"glossary/#widget-options","text":"Every page widget can have its appearance and behavior customized. How this is done varies between widget, but can always be done systematically by clicking on the three-dot menu on the top right of a widget and selecting \u201cWidget options\u201d.","title":"Widget Options"},{"location":"glossary/#wrap-text","text":"Normally, content that doesn\u2019t fit in the width of a cell is truncated, with \u201c\u2026\u201d indicating that part of the data is hidden. When \u201cWrap Text\u201d setting is enabled, long lines will wrap, and the cell will get taller to include all content. An example of wrapping is given in the CRM tutorial .","title":"Wrap Text"},{"location":"imports/","text":"Importing more data # You can import a file to start a new Grist document , or to add data to an existing document. Grist supports imports of Excel, CSV, JSON, tab-separated files and from Google Drive . To start a new Grist document, click the \u201cAdd New\u201d button on the home screen and choose \u201cImport document\u201d, as described in starting a new Grist document . To add to an existing document, open that document, click the \u201cAdd New\u201d button and then \u201cImport from file\u201d. By default, each imported table is added as a new Grist table, but when examining the preview dialog for an import, you have an option to change the destination to an existing Grist table. You can also import any of the same formats from a URL, using the \u201cImport from URL\u201d option. The Import dialog # When you import data into an existing document, Grist opens an import dialog to show you what will be imported. This dialog offers available import options, lets you choose whether to create a new table or add to an existing one, and shows a preview of the data. The \u201cImport options\u201d link on the top right is sometimes useful when importing delimited files. Grist guesses the settings to parse the data (such as the field delimiter), but if it guesses incorrectly, you can adjust the settings. Guessing data structure # In all cases, when you import a file, Grist makes guesses about the structure of the file. For Excel files, Grist treats each sheet as a separated table. For CSV and other delimited formats, one file becomes one table. For both Excel and delimited files, Grist tries to detect whether the headers are included and which line they occur in. If Grist detects there are no headers, it will name columns as \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc. Grist automatically tries to parse numbers, dates, and boolean fields to detect the most suitable type for each column. It tries to be lossless: e.g. if it marks a column as numeric, any text values in it (such as \u201cN/A\u201d) will remain in the imported table, but will be highlighted due to the type mismatch. You can always rename tables and columns after an import, as well as convert types. Import from Google Drive # Importing from a Google Drive is as easy as importing from an Excel file or a CSV file. You can either provide an URL of a file stored in the Google Drive or use a Google File Picker to choose a file from your own drive. To use a Picker, click the \u201cAdd New\u201d button and choose \u201cImport from Google Drive\u201d. To import, sign in to your Google Account by clicking the \u201cSign in\u201d button and following the sign-in process. Grist will ask for a permission to read the file you will import from Google Drive. We won\u2019t read any other files on your drive \u2014 just the single file you choose to import. Once the file has been chosen, the rest of the process is the same as importing from an Excel file. In the import dialog you may configure what data to import, and which destination table to add it to. If you have an URL to a file or a spreadsheet stored on your Google Drive or a file that is publicly accessible, you can import it directly using the \u201cImport from URL\u201d option from the \u201cAdd New\u201d menu. If the file is not shared publicly, Grist will ask you for permission to read files from your Google Drive. If you don\u2019t want to allow Grist to read your files, it will open up Google File Picker where you can select the file you want to import. Import to an existing table # By default, Grist imports new data as new tables, but the Import dialog allows you to change the destination and import data into an existing table. When importing to an existing table, Grist will attempt to match the columns from your imported file to the columns in the destination table. To manually specify column matching, click the \u2018Source Column\u2019 drop down to open a menu with a list of unused columns from your imported file. Click on a column name to match it to a destination column, or select \u2018Skip\u2019 to skip importing data from that column. You can also specify a formula for each imported column by clicking \u2018Apply Formula\u2019 from the \u2018Source Column\u2019 drop down menu. Formulas can reference one or more imported columns, and the result of evaluating the formula will be shown in the preview after closing the editor. Importing to an existing table is best suited for importing multiple datasets containing similar structure. For instance, you could import a bank statement as a new table, then import more statements from other months into the same table. For developers, the Grist API offers a more powerful way to add data to a Grist document. Updating existing records # Suppose the table we are importing to already contains some of the data in our file. We\u2019d like Grist to update these existing records rather than duplicate them. We can tell Grist to update these records by checking the \u201cUpdate existing records\u201d option, and specifying which fields to use for matching incoming data against existing records. An imported row and an existing record that have the same values for all of the selected merge fields are considered the same record. In this case, importing will update the fields of the record with the imported data. Blank values from the imported data will be skipped, leaving the corresponding fields in the original record unchanged. Note: If there are multiple imported rows that have the same values for the selected merge fields, the last row will be used for matching and updating. If there are multiple existing records that have the same values for the selected merge fields, only the first record will be updated if a matching imported row is found. Each time the merge fields are changed, Grist will generate a preview of the updates that will be made to the destination table, and display them in the preview table. Changes are highlighted as follows: New records have all of their fields highlighted in green. Updated records have red and green highlighting for any changed fields: red (with a strikethrough) for existing values from the destination, and green for new values from the imported file. Unchanged records have no highlighting. Field values that exist in the destination table, but are blank in the imported file, are distinguished by a light gray font color.","title":"Importing more data"},{"location":"imports/#importing-more-data","text":"You can import a file to start a new Grist document , or to add data to an existing document. Grist supports imports of Excel, CSV, JSON, tab-separated files and from Google Drive . To start a new Grist document, click the \u201cAdd New\u201d button on the home screen and choose \u201cImport document\u201d, as described in starting a new Grist document . To add to an existing document, open that document, click the \u201cAdd New\u201d button and then \u201cImport from file\u201d. By default, each imported table is added as a new Grist table, but when examining the preview dialog for an import, you have an option to change the destination to an existing Grist table. You can also import any of the same formats from a URL, using the \u201cImport from URL\u201d option.","title":"Importing more data"},{"location":"imports/#the-import-dialog","text":"When you import data into an existing document, Grist opens an import dialog to show you what will be imported. This dialog offers available import options, lets you choose whether to create a new table or add to an existing one, and shows a preview of the data. The \u201cImport options\u201d link on the top right is sometimes useful when importing delimited files. Grist guesses the settings to parse the data (such as the field delimiter), but if it guesses incorrectly, you can adjust the settings.","title":"The Import dialog"},{"location":"imports/#guessing-data-structure","text":"In all cases, when you import a file, Grist makes guesses about the structure of the file. For Excel files, Grist treats each sheet as a separated table. For CSV and other delimited formats, one file becomes one table. For both Excel and delimited files, Grist tries to detect whether the headers are included and which line they occur in. If Grist detects there are no headers, it will name columns as \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc. Grist automatically tries to parse numbers, dates, and boolean fields to detect the most suitable type for each column. It tries to be lossless: e.g. if it marks a column as numeric, any text values in it (such as \u201cN/A\u201d) will remain in the imported table, but will be highlighted due to the type mismatch. You can always rename tables and columns after an import, as well as convert types.","title":"Guessing data structure"},{"location":"imports/#import-from-google-drive","text":"Importing from a Google Drive is as easy as importing from an Excel file or a CSV file. You can either provide an URL of a file stored in the Google Drive or use a Google File Picker to choose a file from your own drive. To use a Picker, click the \u201cAdd New\u201d button and choose \u201cImport from Google Drive\u201d. To import, sign in to your Google Account by clicking the \u201cSign in\u201d button and following the sign-in process. Grist will ask for a permission to read the file you will import from Google Drive. We won\u2019t read any other files on your drive \u2014 just the single file you choose to import. Once the file has been chosen, the rest of the process is the same as importing from an Excel file. In the import dialog you may configure what data to import, and which destination table to add it to. If you have an URL to a file or a spreadsheet stored on your Google Drive or a file that is publicly accessible, you can import it directly using the \u201cImport from URL\u201d option from the \u201cAdd New\u201d menu. If the file is not shared publicly, Grist will ask you for permission to read files from your Google Drive. If you don\u2019t want to allow Grist to read your files, it will open up Google File Picker where you can select the file you want to import.","title":"Import from Google Drive"},{"location":"imports/#import-to-an-existing-table","text":"By default, Grist imports new data as new tables, but the Import dialog allows you to change the destination and import data into an existing table. When importing to an existing table, Grist will attempt to match the columns from your imported file to the columns in the destination table. To manually specify column matching, click the \u2018Source Column\u2019 drop down to open a menu with a list of unused columns from your imported file. Click on a column name to match it to a destination column, or select \u2018Skip\u2019 to skip importing data from that column. You can also specify a formula for each imported column by clicking \u2018Apply Formula\u2019 from the \u2018Source Column\u2019 drop down menu. Formulas can reference one or more imported columns, and the result of evaluating the formula will be shown in the preview after closing the editor. Importing to an existing table is best suited for importing multiple datasets containing similar structure. For instance, you could import a bank statement as a new table, then import more statements from other months into the same table. For developers, the Grist API offers a more powerful way to add data to a Grist document.","title":"Import to an existing table"},{"location":"imports/#updating-existing-records","text":"Suppose the table we are importing to already contains some of the data in our file. We\u2019d like Grist to update these existing records rather than duplicate them. We can tell Grist to update these records by checking the \u201cUpdate existing records\u201d option, and specifying which fields to use for matching incoming data against existing records. An imported row and an existing record that have the same values for all of the selected merge fields are considered the same record. In this case, importing will update the fields of the record with the imported data. Blank values from the imported data will be skipped, leaving the corresponding fields in the original record unchanged. Note: If there are multiple imported rows that have the same values for the selected merge fields, the last row will be used for matching and updating. If there are multiple existing records that have the same values for the selected merge fields, only the first record will be updated if a matching imported row is found. Each time the merge fields are changed, Grist will generate a preview of the updates that will be made to the destination table, and display them in the preview table. Changes are highlighted as follows: New records have all of their fields highlighted in green. Updated records have red and green highlighting for any changed fields: red (with a strikethrough) for existing values from the destination, and green for new values from the imported file. Unchanged records have no highlighting. Field values that exist in the destination table, but are blank in the imported file, are distinguished by a light gray font color.","title":"Updating existing records"},{"location":"integrators/","text":"Integrator Services # Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make Configuring Integrators # Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables. Example: Storing form submissions # Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in! Example: Sending email alerts # We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win! Readiness column # Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example . Triggering (or avoiding triggering) on pre-existing records # The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Integrator Services"},{"location":"integrators/#integrator-services","text":"Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make","title":"Integrator Services"},{"location":"integrators/#configuring-integrators","text":"Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables.","title":"Configuring Integrators"},{"location":"integrators/#example-storing-form-submissions","text":"Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in!","title":"Example: Storing form submissions"},{"location":"integrators/#example-sending-email-alerts","text":"We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win!","title":"Example: Sending email alerts"},{"location":"integrators/#readiness-column","text":"Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example .","title":"Readiness column"},{"location":"integrators/#triggering-or-avoiding-triggering-on-pre-existing-records","text":"The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Triggering (or avoiding triggering) on pre-existing records"},{"location":"investment-research/","text":"How to analyze and visualize data # Grist offers several powerful ways to analyze and visualize data. In this tutorial, you\u2019ll learn how to: Create summary tables Create and configure charts Link charts dynamically To explain these features, we\u2019ll use the sample document \u201cInvestment Research\u201d 1 which includes companies and investments in them up to 2013. Let\u2019s take a look at the sample document and then we\u2019ll talk about how to build it so that you can apply these tools to your own data. Exploring the example # Open the document \u201c Investment Research \u201d, found in Examples & Templates in your Grist home page. The first thing you\u2019ll see is \u201cOverview\u201d. This page contains two charts next to two tables. The top left has a pie chart showing the distribution of investments by category. The table next to it has the same data in tabular form. Below the pie chart is a bar graph showing the total investments raised by year. It is also accompanied by the same data in the table next to it in tabular form. All these charts and tables are examples of \u201csummary tables\u201d, which we\u2019ll describe below. The next page, \u201cBreakdowns\u201d, also contains two tables and two charts, but these are linked dynamically and offer much more detailed insight into the data. On the top left is a table showing the total funding by year (the same table as we saw on the previous page). This table serves as a driver for the chart next to it. When you click on a year in the table, the pie chart updates to show the distribution of investments in that year. Similarly, the bottom table shows investments by category. When you click on any category, the line chart next to it updates to show the history of funding in that category over the years. Note how powerful this is, and how much insight you can gain from it. For instance, you can see that Advertising category has been getting a lot of investment in NY since 2007, but was overtaken by E-commerce in 2013, while the Fashion category had a major spike in 2011. On the next page, \u201cCompany Details\u201d, we get to see the granular data of this dataset. Here, we see a list of companies and the categories they fall into. Each company shown has a link pointing to its listing on the Crunchbase website. Selecting a company shows a card with its details, as well as a list of all the investments it has received. This is where we begin to see the power of Grist. The original dataset is a flat spreadsheet of companies, and an even bigger spreadsheet of investments. By displaying the data graphically, the data comes alive, making it powerful and useful. How can I make this? # With Grist, presenting your own data in graphic form is a few easy steps away. Let\u2019s begin with the first step. Get the data # Let\u2019s import the raw data. We\u2019ll import two CSV files, where each will become its own table. To follow along, save the files from crunchbase_companies_ny.csv and crunchbase_investments_ny.csv to your computer first. Then, create a Grist document by importing the first file from the home page. Next, import the second table using the \u201cAdd New\u201d button and the \u201cImport from file\u201d option. In the import dialog box, finish by clicking \u201cImport\u201d on the bottom left. The tables you\u2019ve imported will be named \u201ccrunchbase_companies_ny\u201d and \u201ccrunchbase_investments_ny\u201d. Click the name at the top of the table to open the dialogue box and rename each of the tables to \u201cCompanies\u201d and \u201cInvestments\u201d. Make it relational # The power of Grist comes from giving structure to the data. Take a look at the \u201cInvestments\u201d table. Sort by the first column and you\u2019ll notice how much repetition there is: each row contains the complete company info, which both duplicates the data in the \u201cCompanies\u201d table, and is repeated multiple times when multiple investments apply to the same company. 2 The reality is that each investment applies to a single company. Each investment row only needs to contain a reference to a company, and the data specific to that investment. To make it so, find a column that identifies a company uniquely. In this dataset, the first column, \u201ccompany_permalink\u201d, does it best 3 . Click on the arrow in the column header and click \u201cColumn Options\u201d. Click on the arrow beside \u201cText\u201d under the \u201cColumn Type\u201d in the dialogue box at the right of the screen and select \u201cReference\u201d from the list. Grist will automatically suggest to make it a \u201cReference\u201d to the \u201cCompanies\u201d table, and to show the referenced company\u2019s \u201cpermalink\u201d. Click \u201cApply\u201d to save this conversion. Let\u2019s also rename this column to \u201cCompany\u201d. In Grist, duplicated data is not needed and we recommend removing it. Using the Option-Minus (Mac) or Alt-Minus (Windows) shortcut is a quick way to remove columns. After removing the columns from \u201ccompany_name\u201d to \u201ccompany_city\u201d, here\u2019s what\u2019s left: The data you\u2019ve deleted isn\u2019t lost since it was duplicated \u2013 it\u2019s still available in the \u201cCompanies\u201d table and can be used in an Investment record\u2019s formula as, e.g. $Company.company_xxx . In fact, there\u2019s a handy way to create this kind of formula. Let\u2019s create one that we\u2019ll need later. Click the header of the \u201cCompany\u201d column. In the right panel\u2019s Column tab, you\u2019ll see a section \u2018Add Referenced Columns\u2019. Click \u2018Add Column\u2019 to add the \u201ccategory_code\u201d column. A new column will be added to the table with the formula $Company.category_code . For each investment, it shows the \u201ccategory_code\u201d of the company linked to its investment record. Summarize # The powerful feature you\u2019ve been waiting for is the one that summarizes the data. Summary tables summarize each numeric column in a data table. We want to find the sum for the funding_total_usd column in the Companies table. Check that the column type is set to \u2018Numeric\u2019 and formatted with $ . To utilize this, let\u2019s add a table showing companies grouped by \u201ccategory_code\u201d. In the \u201cAdd New\u201d menu at the top left, select \u201cAdd Page\u201d. In the dialog box, select \u201cTable\u201d and \u201cCompanies\u201d, and then use the summation icon ( ) to select the \u201cGroup By\u201d columns \u2013 i.e. the columns by which to summarize. If you don\u2019t select any columns, you\u2019ll just get a single row of totals. If you summarize by \u201ccategory_code\u201d, you\u2019ll get a row for each distinct value of \u201ccategory_code\u201d. Let\u2019s do that and then click \u201cAdd Page\u201d. This is similar to Excel\u2019s pivot tables. Each row represents the group of records from the source table (\u201cCompanies\u201d) that have a particular value of \u201ccategory_code\u201d. There is a reminder of that in the table\u2019s title (\u201cCOMPANIES [by category_code]\u201d). Such summary tables can (and should!) use formulas. The columns you choose when creating the table are the identifiers of the groups. All other columns are formula columns \u2013 they are calculated. In formulas, the group of source records summarized by one row is available as the value \u201c$group\u201d. For example, you\u2019ll see a column created automatically called \u201ccount\u201d. If you hit \u201cEnter\u201d, you\u2019ll see the formula in it \u2013 len($group) \u2013 that\u2019s just the number of records in that group of records, i.e. the number of companies in that category. For numeric columns in the source table, summary tables automatically get a same-named numeric column containing a sum, with a formula such as SUM($group.funding_total_usd) . A note for Python fans $group is a special Python object. It\u2019s an iterable collection of records. Using an attribute like $group.A is a shorthand for the list of values in the A column of all the records in the group, i.e. it\u2019s roughly equivalent to [r.A for r in $group] . Sometimes, adding the values doesn\u2019t make sense. E.g. the sum of \u201cfounded_year\u201d is meaningless. It\u2019s best to delete this and any other column we don\u2019t need, which leaves us with the \u201cfunding_total_usd\u201d column. Since this column contains large numbers, it\u2019s a good time to look at the \u201cNumber Format\u201d section of its configuration, and click , (or perhaps $ ) to format numbers to be more readable. Let\u2019s add a second summary table. Select \u201cAdd New\u201d again to \u201cAdd Widget to Page\u201d. To get a summary by year, select the \u201cInvestments\u201d table under \u201cSelect Data\u201d, and again use its sum symbol (\u2211) to select the column by which to summarize: \u201cfunded_year\u201d and then click \u201cAdd to page\u201d. This produces a second summary table that shows a record for each year, with each representing a group of \u201cInvestments\u201d rows for that year. The most useful column is \u201craised_amount_usd\u201d, adding all investments made in that year. Let\u2019s delete the unneeded columns. You\u2019ll notice pink values in \u201craised_amount_usd\u201d. That\u2019s because Grist guesses the column type to be an integer.The pink sums are instances where the numbers exceed Javascript\u2019s ability to handle large integers. To correct for this, the type of the column should be switched to \u201cNumeric\u201d (which trades off precision for the ability to represent very large and very small numbers). Change the type to \u201cNumeric\u201c under \u201cColumn options\u201d. This is again a good time to pick a friendlier number format for the column, and to make it wider to fit the longer numbers. Chart, graph, plot # You can make a chart of any data. To this page, we want to add a graphic version of each summary table. Select the \u201cAdd New\u201d button again, pick \u201cAdd Widget to Page\u201d, select \u201cChart\u201d as the widget, and the same table (Companies) and summary column (category_code) as before. Then click \u201cAdd to Page\u201d. For a chart, you\u2019ll always follow up by customizing it. Open the right panel, and select \u201cChart\u201d tab / \u201cWidget\u201d subtab. For this first chart, under \u201cChart type\u201d, select \u201cPie Chart\u201d. To construct this chart, first select a label, and then select a series to summarize in the pie chart. Since we want the chart to show \u201ccategory_code\u201d as labels, select this series from the \u201cLabel\u201d dropdown. We want to use \u201cfunding_total_usd\u201d as values, so this should be listed at the top of the \u201cseries\u201d list in the configuration panel. As you move your mouse over the items in that list, use the double vertical bars that show up to drag and drop a series at the top of the list. Alternatively, you can hide the other series from the list by clicking the trash icon. Now add a chart showing a trend by year. Add another \u201cWidget to page\u201d, select \u201cChart\u201d under \u201cWidget\u201d, select \u201cInvestments\u201d under \u201cSelect Data\u201d, click summation ( ) to group by \u201cfunded_year\u201d, and click \u201cAdd to Page\u201d. To customize this chart, stick with the chart type \u201cBar Chart\u201d. From the \u201cX-Axis\u201d dropdown, select the column to use for the X (horizontal) axis values. Under \u2018Series\u2019 select a second (and possible additional) column to be values for the Y (vertical) axis. You can rearrange the sections on the screen into a configuration you\u2019d like to see for a dashboard. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. Once you\u2019re finished, rename the page by hovering over the page name then clicking the three-dot icon to open the menu. Select \u201cRename\u201d and rename to \u201cOverview\u201d. Dynamic charts # If you\u2019ve read our other tutorials on linking data, this will come naturally. Charts are simply a different way to show data, and they can be linked in the same way as tables. For our example, we\u2019ll add a new page with a summary table: select widget \u201cTable\u201d, data \u201cInvestments\u201d, group by \u201cfunded_year\u201d, click \u201cAdd Page\u201d. Let\u2019s rename this new page \u201cBreakdowns\u201d. Next, add a widget to this page, selecting widget \u201cChart\u201d, data \u201cInvestments\u201d. For \u201cGroup By\u201d, we pick two columns: \u201cCompany_category_code\u201d and \u201cfunded_year\u201d. This is why we added the \u201cCompany_category_code\u201d column earlier. We can only group investment records by the category code if we have this code for each investment. The \u201cSelect By\u201d dropdown at the bottom left of the dialog box lists widgets already on the screen that can control the selection of data in the chart we are adding. In \u201cSelect By\u201d, choose \u201cINVESTMENTS [by funded_year]\u201d, and click \u201cAdd to Page\u201d. Note: If you need to make changes to a widget you already added, such as change its type, \u201cGroup By\u201d, or \u201cSelect By\u201d settings, you can always do so from the \u201cData\u201d subtab in the widget settings, using the \u201cEdit Data Selection\u201d button. We want to be able to select a year, and then show a pie chart for that year that displays the total for each category code. The \u201cSelect By\u201d option we chose ensures that only the selected year\u2019s data is used. All that\u2019s left is to change the chart type to \u201cPie Chart\u201d, and set \u201cLabel\u201d to \u201cCompany_category_code\u201d and \u201cSeries\u201d to \u201craised_amount_usd\u201d. Note: Graphs need more screenspace, so our small screenshots will look better if we close the side panels by clicking on the opener icons ( , ). Let\u2019s also sort the table by \u201cfunded_year\u201d. As far as sorting, the highlighted button above the table reminds you that sort settings aren\u2019t saved automatically. Click the green button and select \u201cSave\u201d to do that. What\u2019s the result? We can click through the years (or use arrow keys), and see the distribution by category change. Note: If clicking through the years does not affect the chart, the chart must not be linked. You can check and correct it by using the \u201cthree dots\u201d menu on top right of the chart, clicking \u201cData selection\u201d, and ensuring that \u201cSelect By\u201d dropdown is showing \u201cINVESTMENTS [by funded_year]\u201d. To complete the example, we will add two more sections to this \u201cBreakdowns\u201d page. One will be a table listing company categories, and linked to it will be a chart showing the amount of investment in that category over the years. To add the table of categories, use \u201cAdd Widget to Page\u201d, and select \u201cTable\u201d widget, \u201cInvestments\u201d data, grouped by \u201cCompany_category_code\u201d. The \u201cfunded_year\u201d column in the resulting table is meaningless, and should be deleted. For the last step, we add another chart. We need to remember to group by both \u201cCompany_category_code\u201d \u201cfunded_year\u201d, and to set a suitable \u201cSelect By\u201d widget for it. Since there are two tables on this page, you have a choice of which one will drive the data in this chart. In this case, pick the widget that we just added: \u201cINVESTMENTS [by Company_category_code]\u201d. As in the previous section, we configure the chart by selecting \u201cChart Type\u201d as \u201cBar Chart\u201d, and in the \u201cX-Axis\u201d dropdown, selecting \u201cfunded_year\u201d and under \u201cSeries\u201d, selecting \u201craised_amount_usd\u201d and hiding the rest. We can now click through the categories, and see the history of investment into each one. Next steps # If you\u2019re unfamiliar with how we created the \u201cCompany Details\u201d page that\u2019s present in the example, visit one of these earlier tutorials to learn how: \u2018How to build a Lightweight CRM\u2019, or \u2018Managing your Business in Grist\u2019. That\u2019s it! Now go analyze some data! Download crunchbase_companies_ny.csv and crunchbase_investments_ny.csv . The sample data includes only the \u201ccompanies\u201d and \u201cinvestments\u201d data, and includes only New York companies to keep it smaller and faster. The dataset comes from Kaggle . \u21a9 Such duplication is commonly seen in spreadsheets. Data in this form is called \u201cdenormalized\u201d. \u21a9 If you don\u2019t have a single identifying column, you can construct one with a formula. \u21a9","title":"Analyze and visualize"},{"location":"investment-research/#how-to-analyze-and-visualize-data","text":"Grist offers several powerful ways to analyze and visualize data. In this tutorial, you\u2019ll learn how to: Create summary tables Create and configure charts Link charts dynamically To explain these features, we\u2019ll use the sample document \u201cInvestment Research\u201d 1 which includes companies and investments in them up to 2013. Let\u2019s take a look at the sample document and then we\u2019ll talk about how to build it so that you can apply these tools to your own data.","title":""},{"location":"investment-research/#exploring-the-example","text":"Open the document \u201c Investment Research \u201d, found in Examples & Templates in your Grist home page. The first thing you\u2019ll see is \u201cOverview\u201d. This page contains two charts next to two tables. The top left has a pie chart showing the distribution of investments by category. The table next to it has the same data in tabular form. Below the pie chart is a bar graph showing the total investments raised by year. It is also accompanied by the same data in the table next to it in tabular form. All these charts and tables are examples of \u201csummary tables\u201d, which we\u2019ll describe below. The next page, \u201cBreakdowns\u201d, also contains two tables and two charts, but these are linked dynamically and offer much more detailed insight into the data. On the top left is a table showing the total funding by year (the same table as we saw on the previous page). This table serves as a driver for the chart next to it. When you click on a year in the table, the pie chart updates to show the distribution of investments in that year. Similarly, the bottom table shows investments by category. When you click on any category, the line chart next to it updates to show the history of funding in that category over the years. Note how powerful this is, and how much insight you can gain from it. For instance, you can see that Advertising category has been getting a lot of investment in NY since 2007, but was overtaken by E-commerce in 2013, while the Fashion category had a major spike in 2011. On the next page, \u201cCompany Details\u201d, we get to see the granular data of this dataset. Here, we see a list of companies and the categories they fall into. Each company shown has a link pointing to its listing on the Crunchbase website. Selecting a company shows a card with its details, as well as a list of all the investments it has received. This is where we begin to see the power of Grist. The original dataset is a flat spreadsheet of companies, and an even bigger spreadsheet of investments. By displaying the data graphically, the data comes alive, making it powerful and useful.","title":"Exploring the example"},{"location":"investment-research/#how-can-i-make-this","text":"With Grist, presenting your own data in graphic form is a few easy steps away. Let\u2019s begin with the first step.","title":""},{"location":"investment-research/#get-the-data","text":"Let\u2019s import the raw data. We\u2019ll import two CSV files, where each will become its own table. To follow along, save the files from crunchbase_companies_ny.csv and crunchbase_investments_ny.csv to your computer first. Then, create a Grist document by importing the first file from the home page. Next, import the second table using the \u201cAdd New\u201d button and the \u201cImport from file\u201d option. In the import dialog box, finish by clicking \u201cImport\u201d on the bottom left. The tables you\u2019ve imported will be named \u201ccrunchbase_companies_ny\u201d and \u201ccrunchbase_investments_ny\u201d. Click the name at the top of the table to open the dialogue box and rename each of the tables to \u201cCompanies\u201d and \u201cInvestments\u201d.","title":"Get the data"},{"location":"investment-research/#make-it-relational","text":"The power of Grist comes from giving structure to the data. Take a look at the \u201cInvestments\u201d table. Sort by the first column and you\u2019ll notice how much repetition there is: each row contains the complete company info, which both duplicates the data in the \u201cCompanies\u201d table, and is repeated multiple times when multiple investments apply to the same company. 2 The reality is that each investment applies to a single company. Each investment row only needs to contain a reference to a company, and the data specific to that investment. To make it so, find a column that identifies a company uniquely. In this dataset, the first column, \u201ccompany_permalink\u201d, does it best 3 . Click on the arrow in the column header and click \u201cColumn Options\u201d. Click on the arrow beside \u201cText\u201d under the \u201cColumn Type\u201d in the dialogue box at the right of the screen and select \u201cReference\u201d from the list. Grist will automatically suggest to make it a \u201cReference\u201d to the \u201cCompanies\u201d table, and to show the referenced company\u2019s \u201cpermalink\u201d. Click \u201cApply\u201d to save this conversion. Let\u2019s also rename this column to \u201cCompany\u201d. In Grist, duplicated data is not needed and we recommend removing it. Using the Option-Minus (Mac) or Alt-Minus (Windows) shortcut is a quick way to remove columns. After removing the columns from \u201ccompany_name\u201d to \u201ccompany_city\u201d, here\u2019s what\u2019s left: The data you\u2019ve deleted isn\u2019t lost since it was duplicated \u2013 it\u2019s still available in the \u201cCompanies\u201d table and can be used in an Investment record\u2019s formula as, e.g. $Company.company_xxx . In fact, there\u2019s a handy way to create this kind of formula. Let\u2019s create one that we\u2019ll need later. Click the header of the \u201cCompany\u201d column. In the right panel\u2019s Column tab, you\u2019ll see a section \u2018Add Referenced Columns\u2019. Click \u2018Add Column\u2019 to add the \u201ccategory_code\u201d column. A new column will be added to the table with the formula $Company.category_code . For each investment, it shows the \u201ccategory_code\u201d of the company linked to its investment record.","title":"Make it relational"},{"location":"investment-research/#summarize","text":"The powerful feature you\u2019ve been waiting for is the one that summarizes the data. Summary tables summarize each numeric column in a data table. We want to find the sum for the funding_total_usd column in the Companies table. Check that the column type is set to \u2018Numeric\u2019 and formatted with $ . To utilize this, let\u2019s add a table showing companies grouped by \u201ccategory_code\u201d. In the \u201cAdd New\u201d menu at the top left, select \u201cAdd Page\u201d. In the dialog box, select \u201cTable\u201d and \u201cCompanies\u201d, and then use the summation icon ( ) to select the \u201cGroup By\u201d columns \u2013 i.e. the columns by which to summarize. If you don\u2019t select any columns, you\u2019ll just get a single row of totals. If you summarize by \u201ccategory_code\u201d, you\u2019ll get a row for each distinct value of \u201ccategory_code\u201d. Let\u2019s do that and then click \u201cAdd Page\u201d. This is similar to Excel\u2019s pivot tables. Each row represents the group of records from the source table (\u201cCompanies\u201d) that have a particular value of \u201ccategory_code\u201d. There is a reminder of that in the table\u2019s title (\u201cCOMPANIES [by category_code]\u201d). Such summary tables can (and should!) use formulas. The columns you choose when creating the table are the identifiers of the groups. All other columns are formula columns \u2013 they are calculated. In formulas, the group of source records summarized by one row is available as the value \u201c$group\u201d. For example, you\u2019ll see a column created automatically called \u201ccount\u201d. If you hit \u201cEnter\u201d, you\u2019ll see the formula in it \u2013 len($group) \u2013 that\u2019s just the number of records in that group of records, i.e. the number of companies in that category. For numeric columns in the source table, summary tables automatically get a same-named numeric column containing a sum, with a formula such as SUM($group.funding_total_usd) . A note for Python fans $group is a special Python object. It\u2019s an iterable collection of records. Using an attribute like $group.A is a shorthand for the list of values in the A column of all the records in the group, i.e. it\u2019s roughly equivalent to [r.A for r in $group] . Sometimes, adding the values doesn\u2019t make sense. E.g. the sum of \u201cfounded_year\u201d is meaningless. It\u2019s best to delete this and any other column we don\u2019t need, which leaves us with the \u201cfunding_total_usd\u201d column. Since this column contains large numbers, it\u2019s a good time to look at the \u201cNumber Format\u201d section of its configuration, and click , (or perhaps $ ) to format numbers to be more readable. Let\u2019s add a second summary table. Select \u201cAdd New\u201d again to \u201cAdd Widget to Page\u201d. To get a summary by year, select the \u201cInvestments\u201d table under \u201cSelect Data\u201d, and again use its sum symbol (\u2211) to select the column by which to summarize: \u201cfunded_year\u201d and then click \u201cAdd to page\u201d. This produces a second summary table that shows a record for each year, with each representing a group of \u201cInvestments\u201d rows for that year. The most useful column is \u201craised_amount_usd\u201d, adding all investments made in that year. Let\u2019s delete the unneeded columns. You\u2019ll notice pink values in \u201craised_amount_usd\u201d. That\u2019s because Grist guesses the column type to be an integer.The pink sums are instances where the numbers exceed Javascript\u2019s ability to handle large integers. To correct for this, the type of the column should be switched to \u201cNumeric\u201d (which trades off precision for the ability to represent very large and very small numbers). Change the type to \u201cNumeric\u201c under \u201cColumn options\u201d. This is again a good time to pick a friendlier number format for the column, and to make it wider to fit the longer numbers.","title":"Summarize"},{"location":"investment-research/#chart-graph-plot","text":"You can make a chart of any data. To this page, we want to add a graphic version of each summary table. Select the \u201cAdd New\u201d button again, pick \u201cAdd Widget to Page\u201d, select \u201cChart\u201d as the widget, and the same table (Companies) and summary column (category_code) as before. Then click \u201cAdd to Page\u201d. For a chart, you\u2019ll always follow up by customizing it. Open the right panel, and select \u201cChart\u201d tab / \u201cWidget\u201d subtab. For this first chart, under \u201cChart type\u201d, select \u201cPie Chart\u201d. To construct this chart, first select a label, and then select a series to summarize in the pie chart. Since we want the chart to show \u201ccategory_code\u201d as labels, select this series from the \u201cLabel\u201d dropdown. We want to use \u201cfunding_total_usd\u201d as values, so this should be listed at the top of the \u201cseries\u201d list in the configuration panel. As you move your mouse over the items in that list, use the double vertical bars that show up to drag and drop a series at the top of the list. Alternatively, you can hide the other series from the list by clicking the trash icon. Now add a chart showing a trend by year. Add another \u201cWidget to page\u201d, select \u201cChart\u201d under \u201cWidget\u201d, select \u201cInvestments\u201d under \u201cSelect Data\u201d, click summation ( ) to group by \u201cfunded_year\u201d, and click \u201cAdd to Page\u201d. To customize this chart, stick with the chart type \u201cBar Chart\u201d. From the \u201cX-Axis\u201d dropdown, select the column to use for the X (horizontal) axis values. Under \u2018Series\u2019 select a second (and possible additional) column to be values for the Y (vertical) axis. You can rearrange the sections on the screen into a configuration you\u2019d like to see for a dashboard. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. Once you\u2019re finished, rename the page by hovering over the page name then clicking the three-dot icon to open the menu. Select \u201cRename\u201d and rename to \u201cOverview\u201d.","title":"Chart, graph, plot"},{"location":"investment-research/#dynamic-charts","text":"If you\u2019ve read our other tutorials on linking data, this will come naturally. Charts are simply a different way to show data, and they can be linked in the same way as tables. For our example, we\u2019ll add a new page with a summary table: select widget \u201cTable\u201d, data \u201cInvestments\u201d, group by \u201cfunded_year\u201d, click \u201cAdd Page\u201d. Let\u2019s rename this new page \u201cBreakdowns\u201d. Next, add a widget to this page, selecting widget \u201cChart\u201d, data \u201cInvestments\u201d. For \u201cGroup By\u201d, we pick two columns: \u201cCompany_category_code\u201d and \u201cfunded_year\u201d. This is why we added the \u201cCompany_category_code\u201d column earlier. We can only group investment records by the category code if we have this code for each investment. The \u201cSelect By\u201d dropdown at the bottom left of the dialog box lists widgets already on the screen that can control the selection of data in the chart we are adding. In \u201cSelect By\u201d, choose \u201cINVESTMENTS [by funded_year]\u201d, and click \u201cAdd to Page\u201d. Note: If you need to make changes to a widget you already added, such as change its type, \u201cGroup By\u201d, or \u201cSelect By\u201d settings, you can always do so from the \u201cData\u201d subtab in the widget settings, using the \u201cEdit Data Selection\u201d button. We want to be able to select a year, and then show a pie chart for that year that displays the total for each category code. The \u201cSelect By\u201d option we chose ensures that only the selected year\u2019s data is used. All that\u2019s left is to change the chart type to \u201cPie Chart\u201d, and set \u201cLabel\u201d to \u201cCompany_category_code\u201d and \u201cSeries\u201d to \u201craised_amount_usd\u201d. Note: Graphs need more screenspace, so our small screenshots will look better if we close the side panels by clicking on the opener icons ( , ). Let\u2019s also sort the table by \u201cfunded_year\u201d. As far as sorting, the highlighted button above the table reminds you that sort settings aren\u2019t saved automatically. Click the green button and select \u201cSave\u201d to do that. What\u2019s the result? We can click through the years (or use arrow keys), and see the distribution by category change. Note: If clicking through the years does not affect the chart, the chart must not be linked. You can check and correct it by using the \u201cthree dots\u201d menu on top right of the chart, clicking \u201cData selection\u201d, and ensuring that \u201cSelect By\u201d dropdown is showing \u201cINVESTMENTS [by funded_year]\u201d. To complete the example, we will add two more sections to this \u201cBreakdowns\u201d page. One will be a table listing company categories, and linked to it will be a chart showing the amount of investment in that category over the years. To add the table of categories, use \u201cAdd Widget to Page\u201d, and select \u201cTable\u201d widget, \u201cInvestments\u201d data, grouped by \u201cCompany_category_code\u201d. The \u201cfunded_year\u201d column in the resulting table is meaningless, and should be deleted. For the last step, we add another chart. We need to remember to group by both \u201cCompany_category_code\u201d \u201cfunded_year\u201d, and to set a suitable \u201cSelect By\u201d widget for it. Since there are two tables on this page, you have a choice of which one will drive the data in this chart. In this case, pick the widget that we just added: \u201cINVESTMENTS [by Company_category_code]\u201d. As in the previous section, we configure the chart by selecting \u201cChart Type\u201d as \u201cBar Chart\u201d, and in the \u201cX-Axis\u201d dropdown, selecting \u201cfunded_year\u201d and under \u201cSeries\u201d, selecting \u201craised_amount_usd\u201d and hiding the rest. We can now click through the categories, and see the history of investment into each one.","title":"Dynamic charts"},{"location":"investment-research/#next-steps","text":"If you\u2019re unfamiliar with how we created the \u201cCompany Details\u201d page that\u2019s present in the example, visit one of these earlier tutorials to learn how: \u2018How to build a Lightweight CRM\u2019, or \u2018Managing your Business in Grist\u2019. That\u2019s it! Now go analyze some data! Download crunchbase_companies_ny.csv and crunchbase_investments_ny.csv . The sample data includes only the \u201ccompanies\u201d and \u201cinvestments\u201d data, and includes only New York companies to keep it smaller and faster. The dataset comes from Kaggle . \u21a9 Such duplication is commonly seen in spreadsheets. Data in this form is called \u201cdenormalized\u201d. \u21a9 If you don\u2019t have a single identifying column, you can construct one with a formula. \u21a9","title":"Next steps"},{"location":"keyboard-shortcuts/","text":"Grist Shortcuts # General # Key (Mac) Key (Windows) Description F1 , \u2318 / F1 , Ctrl + / Display shortcuts pane \u2318 Z Ctrl + Z Undo last action \u2318 \u21e7 Z , \u2303 Y Ctrl + Shift + Z , Ctrl + Y Redo last action \u2318 F Ctrl + F Find \u2318 G Ctrl + G Find next occurrence \u2318 \u21e7 G Ctrl + Shift + G Find previous occurrence Navigation # Key (Mac) Key (Windows) Description \u2193 \u2193 Move downward to next record or field \u2191 \u2191 Move upward to previous record or field \u2192 \u2192 Move right to the next field \u2190 \u2190 Move left to the previous field Tab Tab Move to the next field, saving changes if editing a value \u21e7 Tab Shift + Tab Move to the previous field, saving changes if editing a value PageDown PageDown Move down one page of records, or to next record in a card list PageUp PageUp Move up one page of records, or to previous record in a card list \u2318 \u2191 Ctrl + \u2191 Move up to the first record \u2318 \u2193 Ctrl + \u2193 Move down to the last record Home Home Move to the first field or the beginning of a row End End Move to the last field or the end of a row \u2325 \u2193 Alt + \u2193 Open next page \u2325 \u2191 Alt + \u2191 Open previous page \u2318 O Ctrl + O Activate next page widget \u2318 \u21e7 O Ctrl + Shift + O Activate previous page widget Space Space Opens a record card in a table widget Selection # Key (Mac) Key (Windows) Description \u21e7 \u2193 Shift + \u2193 Adds the element below the cursor to the selected range \u21e7 \u2191 Shift + \u2191 Adds the element above the cursor to the selected range \u21e7 \u2192 Shift + \u2192 Adds the element to the right of the cursor to the selected range \u21e7 \u2190 Shift + \u2190 Adds the element to the left of the cursor to the selected range \u2318 A Ctrl + A Selects all currently displayed cells \u2318 Shift \u2191 Ctrl + Shift + \u2191 Selects cells above the selected cell in the same column \u2318 Shift \u2193 Ctrl + Shift + \u2193 Selects cells below the selected cell in the same column \u2318 Shift \u2192 Ctrl + Shift + \u2192 Selects cells to the right of the selected cell in the same row \u2318 Shift \u2190 Ctrl + Shift + \u2190 Selects cells to the left of the selected cell in the same row \u2318 \u21e7 A Ctrl + Shift + A Copy anchor link Editing # Key (Mac) Key (Windows) Description Enter , F2 Enter , F2 Start editing the currently-selected cell Enter Enter Finish editing a cell, saving the value Escape Escape Discard changes to a cell value \u2318 D Ctrl + D Fills current selection with the contents of the top row in the selection Delete Backspace , Delete Clears the currently selected cells Enter Enter Toggles the value of checkbox cells = = When typed at the start of a cell, make this a formula column \u2318 ; Ctrl + ; Insert the current date \u2318 \u21e7 ; Ctrl + Shift + ; Insert the current date and time Data manipulation # Key (Mac) Key (Windows) Description \u2318 \u21e7 Enter Ctrl + Shift + Enter Insert a new record, before the currently selected one in an unsorted table \u2318 Enter Ctrl + Enter Insert a new record, after the currently selected one in an unsorted table \u2318 \u21e7 D Ctrl + Shift + D Duplicate the currently selected record(s) \u2318 Delete Ctrl + Backspace , Ctrl + Delete Delete the currently selected record(s) \u2325 \u21e7 = Alt + Shift + = Insert a new column, before the currently selected one \u2325 = Alt + = Insert a new column, after the currently selected one \u2303 M Ctrl + M Rename the currently selected column \u2325 \u21e7 - Alt + Shift + - Hide the currently selected column \u2325 - Alt + - Delete the currently selected columns","title":"Shortcuts"},{"location":"keyboard-shortcuts/#grist-shortcuts","text":"","title":"Grist Shortcuts"},{"location":"keyboard-shortcuts/#general","text":"Key (Mac) Key (Windows) Description F1 , \u2318 / F1 , Ctrl + / Display shortcuts pane \u2318 Z Ctrl + Z Undo last action \u2318 \u21e7 Z , \u2303 Y Ctrl + Shift + Z , Ctrl + Y Redo last action \u2318 F Ctrl + F Find \u2318 G Ctrl + G Find next occurrence \u2318 \u21e7 G Ctrl + Shift + G Find previous occurrence","title":"General"},{"location":"keyboard-shortcuts/#navigation","text":"Key (Mac) Key (Windows) Description \u2193 \u2193 Move downward to next record or field \u2191 \u2191 Move upward to previous record or field \u2192 \u2192 Move right to the next field \u2190 \u2190 Move left to the previous field Tab Tab Move to the next field, saving changes if editing a value \u21e7 Tab Shift + Tab Move to the previous field, saving changes if editing a value PageDown PageDown Move down one page of records, or to next record in a card list PageUp PageUp Move up one page of records, or to previous record in a card list \u2318 \u2191 Ctrl + \u2191 Move up to the first record \u2318 \u2193 Ctrl + \u2193 Move down to the last record Home Home Move to the first field or the beginning of a row End End Move to the last field or the end of a row \u2325 \u2193 Alt + \u2193 Open next page \u2325 \u2191 Alt + \u2191 Open previous page \u2318 O Ctrl + O Activate next page widget \u2318 \u21e7 O Ctrl + Shift + O Activate previous page widget Space Space Opens a record card in a table widget","title":"Navigation"},{"location":"keyboard-shortcuts/#selection","text":"Key (Mac) Key (Windows) Description \u21e7 \u2193 Shift + \u2193 Adds the element below the cursor to the selected range \u21e7 \u2191 Shift + \u2191 Adds the element above the cursor to the selected range \u21e7 \u2192 Shift + \u2192 Adds the element to the right of the cursor to the selected range \u21e7 \u2190 Shift + \u2190 Adds the element to the left of the cursor to the selected range \u2318 A Ctrl + A Selects all currently displayed cells \u2318 Shift \u2191 Ctrl + Shift + \u2191 Selects cells above the selected cell in the same column \u2318 Shift \u2193 Ctrl + Shift + \u2193 Selects cells below the selected cell in the same column \u2318 Shift \u2192 Ctrl + Shift + \u2192 Selects cells to the right of the selected cell in the same row \u2318 Shift \u2190 Ctrl + Shift + \u2190 Selects cells to the left of the selected cell in the same row \u2318 \u21e7 A Ctrl + Shift + A Copy anchor link","title":"Selection"},{"location":"keyboard-shortcuts/#editing","text":"Key (Mac) Key (Windows) Description Enter , F2 Enter , F2 Start editing the currently-selected cell Enter Enter Finish editing a cell, saving the value Escape Escape Discard changes to a cell value \u2318 D Ctrl + D Fills current selection with the contents of the top row in the selection Delete Backspace , Delete Clears the currently selected cells Enter Enter Toggles the value of checkbox cells = = When typed at the start of a cell, make this a formula column \u2318 ; Ctrl + ; Insert the current date \u2318 \u21e7 ; Ctrl + Shift + ; Insert the current date and time","title":"Editing"},{"location":"keyboard-shortcuts/#data-manipulation","text":"Key (Mac) Key (Windows) Description \u2318 \u21e7 Enter Ctrl + Shift + Enter Insert a new record, before the currently selected one in an unsorted table \u2318 Enter Ctrl + Enter Insert a new record, after the currently selected one in an unsorted table \u2318 \u21e7 D Ctrl + Shift + D Duplicate the currently selected record(s) \u2318 Delete Ctrl + Backspace , Ctrl + Delete Delete the currently selected record(s) \u2325 \u21e7 = Alt + Shift + = Insert a new column, before the currently selected one \u2325 = Alt + = Insert a new column, after the currently selected one \u2303 M Ctrl + M Rename the currently selected column \u2325 \u21e7 - Alt + Shift + - Hide the currently selected column \u2325 - Alt + - Delete the currently selected columns","title":"Data manipulation"},{"location":"lightweight-crm/","text":"How to create a custom CRM # Grist is as easy to use as a spreadsheet, but gives you new powers when data doesn\u2019t fit in a simple grid. A good example is keeping track of contacts and our conversations with them. For a business, this could be customers, sales leads, or job candidates. For an individual, it could be companies they have applied to in a job search. In this tutorial, we\u2019ll explain the \u201cLightweight CRM\u201d example, which you can use as a template for your own contacts, and then show how to build it from scratch. You\u2019ll learn how to: Add tables Link data Set column types Create custom layouts Exploring the example # Open the document Lightweight CRM , found in Examples & Templates in your Grist home page . You\u2019ll see the \u201cContacts\u201d page, listing contacts on the left of the screen. Click on any contact to select it. The two sections on the right show the details and the history of interactions with the selected contact. How does this compare to a spreadsheet? These screenshots show the Lightweight CRM example on the left, and a regular spreadsheet with the same data on the right. Previous Next Previous Next The difficulty is in the history of notes for a contact. In a two-dimensional grid, you have few options for where to include multiple notes. If you include them as multiple columns, it quickly makes the spreadsheet unwieldy and difficult to navigate. Grist feels more like an application, but it\u2019s still as versatile as a spreadsheet. The \u201cLightweight CRM\u201d example can be used immediately as-is (with the sample data), or as a template (just the structure without the data). Here are a few more points on using it as a CRM: To add a new contact, click the blank row at the bottom of the contacts list, then fill in the blank \u201cCONTACTS Card\u201d section that shows on the right. To add a new conversation, select a contact, then click the blank line at the end of the Interactions table. You can enter today\u2019s date using the shortcut \u2318 + ; (semicolon) (Mac) or Ctrl + ; (semicolon) (Windows). Then select the type of interaction using auto-complete, and type in your notes. You can add To-Do items for a contact: in the Interactions list, select \u201cTo-Do\u201d in the \u201cType\u201d column as a special type of interaction. Think of the associated date as the due date for this task. The Contacts table shows the list of coming up To-Do items, sorted by their due date. If you use Gmail, the handy \u201cGmail search\u201d link in the \u201cCONTACTS Card\u201d section will open a browser window with the Gmail search results for this contact\u2019s email address. You can use this example as a template for your own contacts. With the \u201cLightweight CRM\u201d example open, click the \u201cSave Copy\u201d button in in the top bar, then mark the \u201cAs Template\u201d checkbox. You\u2019ll get an empty document with the same layout, and can start filling it in with your own data. If you aren\u2019t signed in, you will need to sign in to make a copy of the example. Creating your own # The rest of this tutorial will show you how to create such a document on your own. It\u2019s a great exercise that will teach you some of the key features of Grist. To start, we\u2019ll import a file with sample contacts from the Grist home page. First, save this file to your desktop: lightweight-crm-contacts.csv . Then click the \u201cAdd New\u201d button on the top left of your Grist home page, click \u201cImport document\u201d, and select the file on your desktop. You\u2019ll see a table of contacts with sample data. Note that in Grist, columns have names. Rename this table to \u201cContacts\u201d by clicking its name in the top bar, and typing the new name. That\u2019s all you need for a simple table of contacts. You can add rows here, or add new columns to associate more data with each contact. Adding another table # For our next step, we want to be able to select a contact, and see the list of conversations with that contact. These conversations should be a new table of data. The cue is that it has a different number of rows from the table of contacts. Create the new table using the green \u201cAdd New\u201d button on the top left of your screen, and click \u201cAdd Empty Table\u201d in the menu. This table will represent interactions with our contacts, so let\u2019s rename it \u201cInteractions\u201d by clicking its default name (\u201cTable1\u201d) on top of the screen, as before. It\u2019s a good idea to give meaningful names to columns. In this case, for each interaction, we need to know which Contact it refers to, the date, type, and conversation notes. To rename a column, click its header to select the column, and click the header again to edit its name. You can hit the Tab key to continue to renaming the next column. Finally, hit the \u201c+\u201d button to the right of the last column to create one more column, and name it \u201cNotes\u201d. Linking data records # Every record in this table will belong to a particular contact. You set it up by turning the \u201cContact\u201d column into a reference to the table \u201cContacts\u201d. Using the triangle in the header of the column \u201cContact\u201d, open the menu and select \u201cColumn Options\u201d. In the right panel, use the \u201cColumn Type\u201d dropdown to select \u201cReference\u201d, then under \u201cData from table\u201d, select \u201cContacts\u201d. Each cell in this column will hold a pointer to a row in the \u201cContacts\u201d table 1 . While it refers to an entire row, it\u2019s useful to see some particular identifier of that row, so under \u201cShow column\u201d, select \u201cCompany\u201d. You\u2019ll see this in action shortly. Setting other types # In Grist, every column has a type. Often, the default of Text or Numeric is correct. For our \u201cDate\u201d column, a better type is Date. Click any cell in the \u201cDate\u201d column, and in the right panel, click into the \u201cColumn Type\u201d dropdown and select \u201cDate\u201d. If you\u2019d like, you can also choose a different date format right below the type. Now, if you click on a cell in the \u201cDate\u201d column and hit Enter, you have a convenient date picker. Another useful column type for us is \u201cChoice\u201d. Our interactions will be either \u201cPhone\u201d, \u201cEmail\u201d, or \u201cIn-person\u201d, and it\u2019s useful to list these options. Click into the \u201cType\u201d column, and in the right panel, set \u201cColumn Type\u201d to \u201cChoice\u201d. You\u2019ll see \u201cChoice Values\u201d textbox below. Click it, and enter your choices there, one per line: \u201cPhone\u201d, \u201cEmail\u201d, \u201cIn-person\u201d. Now, if you click on a cell in the \u201cType\u201d column and hit Enter, you can now choose from among the choices you set, or start typing and use auto-complete. Linking tables visually # The next step is to link the two tables visually. Open the \u201cContacts\u201d page, click the \u201cAdd New\u201d button on top of the left panel, then \u201cAdd Widget to Page\u201d. Select widget \u201cTable\u201d and data \u201cInteractions\u201d. In the \u201cSelect By\u201d dropdown at the bottom of the dialog, select \u201cCONTACTS\u201d. This means that choosing a contact will display only the interactions with that contact. Click \u201cAdd to Page\u201d to finish. Next, let\u2019s select a contact in the table on the left (let\u2019s use \u201cDouglas LLC\u201d in the fourth row) and add some notes for it. Type in a date (hint: the shortcut \u2318 + ; (semicolon) on Mac or Ctrl + ; (semicolon) on Windows inserts today\u2019s date), select a type, and enter a note. As soon as that row is created, the \u201cContact\u201d column is automatically filled with \u201cDouglas LLC\u201d, thanks to the sections being linked. The note we added is shown only when \u201cDouglas LLC\u201d is selected. We can add more notes for \u201cDouglas LLC\u201d, or add notes for any of the other contacts. We can now hide the \u201cContact\u201d column in the \u201cInteractions\u201d table: using the menu in the column\u2019s header, select \u201cHide Column\u201d. Because the tables are linked, we already see who the notes are for. For longer notes to be convenient, resize the \u201cNotes\u201d column by dragging the right edge of its header. To wrap long notes, open the Column Options, and click the line-wrapping icon. Customizing layout # Once you have multiple tables on one screen, the layout of the screen may become an issue. Having many columns in the Contacts table may no longer be convenient. It\u2019s better to lay it out like a custom application: select a contact from a list on the left and see that contact\u2019s details and interactions. This can be done by using \u201cAdd Widget to Page\u201d again. This time, we\u2019ll select the widget \u201cCard\u201d for the table \u201cContacts\u201d, and for \u201cSelect By\u201d will again use \u201cCONTACTS\u201d. You can move the resulting sections around to create a convenient layout. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. You can also resize sections by moving the mouse between them to find a dotted line. Drag this line to resize. Note how the same personal data is now shown in two places on the screen. These are not copies of data, but different presentations of the same data. Changing the data in one place will change it in the other. Customizing fields # At this point, we may do some cleanup: hide unneeded columns in the main \u201cContacts\u201d table and rearrange fields in the Card widget. A quick way to hide columns is using the right panel. Using the three-dot menu on the top right of the \u201cContacts\u201d table, select \u201cWidget options\u201d. In the panel that opens, find a list of \u201cVisible columns\u201d. Move the mouse over each column to reveal the \u201ceye\u201d icon. Click it to hide all columns except \u201cCompany\u201d. To customize the Card widget, click it. The right panel will show the relevant options. You can select a different Theme, e.g. \u201cCompact\u201d. To rearrange fields, click \u201cEdit Layout\u201d in the right panel. You can now drag-and-drop fields in the card, resize them, or remove them. Click \u201cSave\u201d once you are done. In a few short steps, we have gone from a clunky, unwieldy spreadsheet to a concise, elegant record of your interactions in a simple, effective custom application. To-Do Tasks for Contacts # The \u201cLightweight CRM\u201d example has another trick up its sleeve. The \u201cType\u201d column in the interactions table has an extra choice, \u201cTo-Do\u201d. After you talk to a contact, you can add an extra note about what you need to do for the next conversation, and the date when it\u2019s due. The \u201cContacts\u201d table makes these To-Do items visible, and sortable by due date. This way you can see at a glance what\u2019s coming up next. If you are interested in the details of setting it up, expand the section below. For your first introduction to Grist, you are welcome to skip it. > Setting up To-Do tasks # To set up To-Do items as in the example, select Column Options for the \u201cType\u201d column in the Interactions table, and add another choice (\u201cTo-Do\u201d) to the list of choices: Let\u2019s pick our contact \u201cDouglas LLC\u201d and add a To-Do item: In the \u201cContacts\u201d table, add two new columns: Rename them to \u201cDue\u201d and \u201cTo-Do Items\u201d. Both columns are calculated using formulas. Grist has great support for formulas, allowing full Python syntax and many Excel functions. In Grist, a formula always applies to the entire column of data. To enter a formula, click on a cell in the \u201cDue\u201d column, and hit \u201c=\u201d key: In this formula, we want to look up all Interactions for the current Contact whose Type is \u201cTo-Do\u201d, then select the one with the earliest Date. Using Python syntax, the formula is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Paste it in, or type in. When typing in multi-line formulas, use Shift+Enter to add new lines, and Enter to save. It\u2019s also a good time to change the column type to \u201cDate\u201d. Open Column Options, and select \u201cDate\u201d for the type. You can choose the Date Format directly below the type. For the \u201cTo-Do Items\u201d, enter a formula similarly. In case of multiple To-Do items, this formula will concatenate them, separated by line breaks. Click into the \u201cTo-Do Items\u201d column, hit \u201c=\u201d to start typing the formula, and enter items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return \"\\n\".join(items.Notes) Now the To-Do item we entered earlier is visible in the main Contacts table. Note that the values in these columns are read-only, since they are calculated. To change the due date, find the To-Do item in the Interactions table where you created it. Sorting tables # We\u2019ll want contacts with To-Do items to show up first, in order of the Due date. Click the triangle in the header of the \u201cDue\u201d column, and select \u201cSort A-Z\u201d. By default, sorting settings are not saved. The highlighted green button in the top right of the \u201cContacts\u201d table reminds us of that. To keep this sort order when you reopen the document, save it by clicking that green button and selecting \u201cSave\u201d. You can also save by clicking the green check mark, to the right of the filter icon. Other features # Grist has more great features, some of which are used in the \u201cLightweight CRM\u201d example document. To read more about those, follow the links to their documentation. Any text column may be shown as a hyperlink . Lightweight CRM example uses it twice: for the \u201cwebsite\u201d field, and for a formula-constructed hyperlink to a Gmail search page for the given contact\u2019s email. The latter is handy if you use Gmail. Grist supports attachments . In the example, there is an \u201cAttachments\u201d field for each contact that may be used to store an image of a business card, for example. In an actual business, you\u2019ll need more. Specialized CRM products have tons of features. Their problem is complexity: trying to satisfy the needs of many different clients makes for a complicated product to use. The beauty of Grist is that you can start simple and add only the level of complexity you need, and nothing more. Other tutorials show how to model more complex data, analyze and chart data, and more. In the database world, this kind of reference or pointer is known as a \u201cforeign key\u201d. \u21a9","title":"Create your own CRM"},{"location":"lightweight-crm/#how-to-create-a-custom-crm","text":"Grist is as easy to use as a spreadsheet, but gives you new powers when data doesn\u2019t fit in a simple grid. A good example is keeping track of contacts and our conversations with them. For a business, this could be customers, sales leads, or job candidates. For an individual, it could be companies they have applied to in a job search. In this tutorial, we\u2019ll explain the \u201cLightweight CRM\u201d example, which you can use as a template for your own contacts, and then show how to build it from scratch. You\u2019ll learn how to: Add tables Link data Set column types Create custom layouts","title":"Intro"},{"location":"lightweight-crm/#exploring-the-example","text":"Open the document Lightweight CRM , found in Examples & Templates in your Grist home page . You\u2019ll see the \u201cContacts\u201d page, listing contacts on the left of the screen. Click on any contact to select it. The two sections on the right show the details and the history of interactions with the selected contact. How does this compare to a spreadsheet? These screenshots show the Lightweight CRM example on the left, and a regular spreadsheet with the same data on the right. Previous Next Previous Next The difficulty is in the history of notes for a contact. In a two-dimensional grid, you have few options for where to include multiple notes. If you include them as multiple columns, it quickly makes the spreadsheet unwieldy and difficult to navigate. Grist feels more like an application, but it\u2019s still as versatile as a spreadsheet. The \u201cLightweight CRM\u201d example can be used immediately as-is (with the sample data), or as a template (just the structure without the data). Here are a few more points on using it as a CRM: To add a new contact, click the blank row at the bottom of the contacts list, then fill in the blank \u201cCONTACTS Card\u201d section that shows on the right. To add a new conversation, select a contact, then click the blank line at the end of the Interactions table. You can enter today\u2019s date using the shortcut \u2318 + ; (semicolon) (Mac) or Ctrl + ; (semicolon) (Windows). Then select the type of interaction using auto-complete, and type in your notes. You can add To-Do items for a contact: in the Interactions list, select \u201cTo-Do\u201d in the \u201cType\u201d column as a special type of interaction. Think of the associated date as the due date for this task. The Contacts table shows the list of coming up To-Do items, sorted by their due date. If you use Gmail, the handy \u201cGmail search\u201d link in the \u201cCONTACTS Card\u201d section will open a browser window with the Gmail search results for this contact\u2019s email address. You can use this example as a template for your own contacts. With the \u201cLightweight CRM\u201d example open, click the \u201cSave Copy\u201d button in in the top bar, then mark the \u201cAs Template\u201d checkbox. You\u2019ll get an empty document with the same layout, and can start filling it in with your own data. If you aren\u2019t signed in, you will need to sign in to make a copy of the example.","title":"Exploring the example"},{"location":"lightweight-crm/#creating-your-own","text":"The rest of this tutorial will show you how to create such a document on your own. It\u2019s a great exercise that will teach you some of the key features of Grist. To start, we\u2019ll import a file with sample contacts from the Grist home page. First, save this file to your desktop: lightweight-crm-contacts.csv . Then click the \u201cAdd New\u201d button on the top left of your Grist home page, click \u201cImport document\u201d, and select the file on your desktop. You\u2019ll see a table of contacts with sample data. Note that in Grist, columns have names. Rename this table to \u201cContacts\u201d by clicking its name in the top bar, and typing the new name. That\u2019s all you need for a simple table of contacts. You can add rows here, or add new columns to associate more data with each contact.","title":"Creating your own"},{"location":"lightweight-crm/#adding-another-table","text":"For our next step, we want to be able to select a contact, and see the list of conversations with that contact. These conversations should be a new table of data. The cue is that it has a different number of rows from the table of contacts. Create the new table using the green \u201cAdd New\u201d button on the top left of your screen, and click \u201cAdd Empty Table\u201d in the menu. This table will represent interactions with our contacts, so let\u2019s rename it \u201cInteractions\u201d by clicking its default name (\u201cTable1\u201d) on top of the screen, as before. It\u2019s a good idea to give meaningful names to columns. In this case, for each interaction, we need to know which Contact it refers to, the date, type, and conversation notes. To rename a column, click its header to select the column, and click the header again to edit its name. You can hit the Tab key to continue to renaming the next column. Finally, hit the \u201c+\u201d button to the right of the last column to create one more column, and name it \u201cNotes\u201d.","title":"Adding another table"},{"location":"lightweight-crm/#linking-data-records","text":"Every record in this table will belong to a particular contact. You set it up by turning the \u201cContact\u201d column into a reference to the table \u201cContacts\u201d. Using the triangle in the header of the column \u201cContact\u201d, open the menu and select \u201cColumn Options\u201d. In the right panel, use the \u201cColumn Type\u201d dropdown to select \u201cReference\u201d, then under \u201cData from table\u201d, select \u201cContacts\u201d. Each cell in this column will hold a pointer to a row in the \u201cContacts\u201d table 1 . While it refers to an entire row, it\u2019s useful to see some particular identifier of that row, so under \u201cShow column\u201d, select \u201cCompany\u201d. You\u2019ll see this in action shortly.","title":"Linking data records"},{"location":"lightweight-crm/#setting-other-types","text":"In Grist, every column has a type. Often, the default of Text or Numeric is correct. For our \u201cDate\u201d column, a better type is Date. Click any cell in the \u201cDate\u201d column, and in the right panel, click into the \u201cColumn Type\u201d dropdown and select \u201cDate\u201d. If you\u2019d like, you can also choose a different date format right below the type. Now, if you click on a cell in the \u201cDate\u201d column and hit Enter, you have a convenient date picker. Another useful column type for us is \u201cChoice\u201d. Our interactions will be either \u201cPhone\u201d, \u201cEmail\u201d, or \u201cIn-person\u201d, and it\u2019s useful to list these options. Click into the \u201cType\u201d column, and in the right panel, set \u201cColumn Type\u201d to \u201cChoice\u201d. You\u2019ll see \u201cChoice Values\u201d textbox below. Click it, and enter your choices there, one per line: \u201cPhone\u201d, \u201cEmail\u201d, \u201cIn-person\u201d. Now, if you click on a cell in the \u201cType\u201d column and hit Enter, you can now choose from among the choices you set, or start typing and use auto-complete.","title":"Setting other types"},{"location":"lightweight-crm/#linking-tables-visually","text":"The next step is to link the two tables visually. Open the \u201cContacts\u201d page, click the \u201cAdd New\u201d button on top of the left panel, then \u201cAdd Widget to Page\u201d. Select widget \u201cTable\u201d and data \u201cInteractions\u201d. In the \u201cSelect By\u201d dropdown at the bottom of the dialog, select \u201cCONTACTS\u201d. This means that choosing a contact will display only the interactions with that contact. Click \u201cAdd to Page\u201d to finish. Next, let\u2019s select a contact in the table on the left (let\u2019s use \u201cDouglas LLC\u201d in the fourth row) and add some notes for it. Type in a date (hint: the shortcut \u2318 + ; (semicolon) on Mac or Ctrl + ; (semicolon) on Windows inserts today\u2019s date), select a type, and enter a note. As soon as that row is created, the \u201cContact\u201d column is automatically filled with \u201cDouglas LLC\u201d, thanks to the sections being linked. The note we added is shown only when \u201cDouglas LLC\u201d is selected. We can add more notes for \u201cDouglas LLC\u201d, or add notes for any of the other contacts. We can now hide the \u201cContact\u201d column in the \u201cInteractions\u201d table: using the menu in the column\u2019s header, select \u201cHide Column\u201d. Because the tables are linked, we already see who the notes are for. For longer notes to be convenient, resize the \u201cNotes\u201d column by dragging the right edge of its header. To wrap long notes, open the Column Options, and click the line-wrapping icon.","title":"Linking tables visually"},{"location":"lightweight-crm/#customizing-layout","text":"Once you have multiple tables on one screen, the layout of the screen may become an issue. Having many columns in the Contacts table may no longer be convenient. It\u2019s better to lay it out like a custom application: select a contact from a list on the left and see that contact\u2019s details and interactions. This can be done by using \u201cAdd Widget to Page\u201d again. This time, we\u2019ll select the widget \u201cCard\u201d for the table \u201cContacts\u201d, and for \u201cSelect By\u201d will again use \u201cCONTACTS\u201d. You can move the resulting sections around to create a convenient layout. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. You can also resize sections by moving the mouse between them to find a dotted line. Drag this line to resize. Note how the same personal data is now shown in two places on the screen. These are not copies of data, but different presentations of the same data. Changing the data in one place will change it in the other.","title":"Customizing layout"},{"location":"lightweight-crm/#customizing-fields","text":"At this point, we may do some cleanup: hide unneeded columns in the main \u201cContacts\u201d table and rearrange fields in the Card widget. A quick way to hide columns is using the right panel. Using the three-dot menu on the top right of the \u201cContacts\u201d table, select \u201cWidget options\u201d. In the panel that opens, find a list of \u201cVisible columns\u201d. Move the mouse over each column to reveal the \u201ceye\u201d icon. Click it to hide all columns except \u201cCompany\u201d. To customize the Card widget, click it. The right panel will show the relevant options. You can select a different Theme, e.g. \u201cCompact\u201d. To rearrange fields, click \u201cEdit Layout\u201d in the right panel. You can now drag-and-drop fields in the card, resize them, or remove them. Click \u201cSave\u201d once you are done. In a few short steps, we have gone from a clunky, unwieldy spreadsheet to a concise, elegant record of your interactions in a simple, effective custom application.","title":"Customizing fields"},{"location":"lightweight-crm/#to-do-tasks-for-contacts","text":"The \u201cLightweight CRM\u201d example has another trick up its sleeve. The \u201cType\u201d column in the interactions table has an extra choice, \u201cTo-Do\u201d. After you talk to a contact, you can add an extra note about what you need to do for the next conversation, and the date when it\u2019s due. The \u201cContacts\u201d table makes these To-Do items visible, and sortable by due date. This way you can see at a glance what\u2019s coming up next. If you are interested in the details of setting it up, expand the section below. For your first introduction to Grist, you are welcome to skip it.","title":"To-Do Tasks for Contacts"},{"location":"lightweight-crm/#setting-up-to-do-tasks","text":"To set up To-Do items as in the example, select Column Options for the \u201cType\u201d column in the Interactions table, and add another choice (\u201cTo-Do\u201d) to the list of choices: Let\u2019s pick our contact \u201cDouglas LLC\u201d and add a To-Do item: In the \u201cContacts\u201d table, add two new columns: Rename them to \u201cDue\u201d and \u201cTo-Do Items\u201d. Both columns are calculated using formulas. Grist has great support for formulas, allowing full Python syntax and many Excel functions. In Grist, a formula always applies to the entire column of data. To enter a formula, click on a cell in the \u201cDue\u201d column, and hit \u201c=\u201d key: In this formula, we want to look up all Interactions for the current Contact whose Type is \u201cTo-Do\u201d, then select the one with the earliest Date. Using Python syntax, the formula is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Paste it in, or type in. When typing in multi-line formulas, use Shift+Enter to add new lines, and Enter to save. It\u2019s also a good time to change the column type to \u201cDate\u201d. Open Column Options, and select \u201cDate\u201d for the type. You can choose the Date Format directly below the type. For the \u201cTo-Do Items\u201d, enter a formula similarly. In case of multiple To-Do items, this formula will concatenate them, separated by line breaks. Click into the \u201cTo-Do Items\u201d column, hit \u201c=\u201d to start typing the formula, and enter items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return \"\\n\".join(items.Notes) Now the To-Do item we entered earlier is visible in the main Contacts table. Note that the values in these columns are read-only, since they are calculated. To change the due date, find the To-Do item in the Interactions table where you created it.","title":""},{"location":"lightweight-crm/#sorting-tables","text":"We\u2019ll want contacts with To-Do items to show up first, in order of the Due date. Click the triangle in the header of the \u201cDue\u201d column, and select \u201cSort A-Z\u201d. By default, sorting settings are not saved. The highlighted green button in the top right of the \u201cContacts\u201d table reminds us of that. To keep this sort order when you reopen the document, save it by clicking that green button and selecting \u201cSave\u201d. You can also save by clicking the green check mark, to the right of the filter icon.","title":""},{"location":"lightweight-crm/#other-features","text":"Grist has more great features, some of which are used in the \u201cLightweight CRM\u201d example document. To read more about those, follow the links to their documentation. Any text column may be shown as a hyperlink . Lightweight CRM example uses it twice: for the \u201cwebsite\u201d field, and for a formula-constructed hyperlink to a Gmail search page for the given contact\u2019s email. The latter is handy if you use Gmail. Grist supports attachments . In the example, there is an \u201cAttachments\u201d field for each contact that may be used to store an image of a business card, for example. In an actual business, you\u2019ll need more. Specialized CRM products have tons of features. Their problem is complexity: trying to satisfy the needs of many different clients makes for a complicated product to use. The beauty of Grist is that you can start simple and add only the level of complexity you need, and nothing more. Other tutorials show how to model more complex data, analyze and chart data, and more. In the database world, this kind of reference or pointer is known as a \u201cforeign key\u201d. \u21a9","title":"Other features"},{"location":"limits/","text":"Limits # To help you assess whether Grist will work for a use-case you have in mind, here is a list of limitations that apply to its operation. Number of documents # On all plans, the number of documents is not limited. To prevent accidental abuse of the system by automation tools, team sites may be limited to 1000 documents. If you encounter such a limit for legitimate use, please contact support to increase it. Older free plans had a limit of ten documents. Learn more about legacy limits. Number of collaborators # For team sites on all plans, there is no limit on the number of team members that may be added to the site. For paid plans, the number of team members determines the price. See our pricing page for details. Team members added to your team site may inherit access to workspaces or documents within that organization. Learn more about team sharing . On both personal and team sites, each document may be shared with up to 2 free guests who do not affect the plan price even on paid plans. Number of tables per document # There is a limit of 500 tables allowed per document. This is a soft limit. If you find yourself with a large number of tables, consider merging ones that have the same structure. For example, if you have a table for each product type, consider using a single table with the product type as an extra column. Rows per document # On the Free plan, documents have a limit of 5,000 rows. On the Pro plan, documents may have up to 100,000 rows. This is a rule of thumb. The actual limit depends also on the number of tables, columns, and the average size of data in each cell. One way to estimate it is to measure the size of the data when it is in CSV format: the limit is around 20MB in this format. For example, a document with 200,000 rows and 12 numeric columns would reach that. Attachments are counted separately. Attachments plus data in a single document are limited to 1GB on all plans. Upload limits # Uploads are limited to 50MB, both for attachments and to import data. Note that an import within this limit may result in a document that exceeds the document size limit, in which case the upload is still likely to fail. API limits # Free plans are limited to 5,000 API calls per document per day. Pro plans raise the limit to 40,000 calls per document per day. Free plans may be rate limited to 5 API requests per second per document. The team plan does not impose such a rate limit. Separately, there is a concurrency limit of 10 for all plans: if 10 authorized API requests are currently being processed for a particular document, any other API requests will be rejected (with HTTP status code 429) until at least one of the original requests completes. A client that waits for one request to complete before sending the next would not hit this limit (assuming it is the sole client accessing the document). The size of the body of any individual API request is limited to 1MB. In particular, this means that requests adding or updating multiple records may need to be split into batches that fit within this limit. Document availability # From time to time, during upgrades and operational transitions, individual Grist documents may become inaccessible for a period of some seconds. Please bear this in mind when using Grist\u2019s API. Legacy limits # Older free personal plans have the following limits: 10 documents per site No workspaces 100,000 rows To determine if you\u2019re on a legacy personal site, click on your site name (@your-name) in the top left. Personal sites on the legacy plan will say \u201cPersonal Site (Legacy)\u201d in the dropdown menu. On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page.","title":"Limits"},{"location":"limits/#limits","text":"To help you assess whether Grist will work for a use-case you have in mind, here is a list of limitations that apply to its operation.","title":"Limits"},{"location":"limits/#number-of-documents","text":"On all plans, the number of documents is not limited. To prevent accidental abuse of the system by automation tools, team sites may be limited to 1000 documents. If you encounter such a limit for legitimate use, please contact support to increase it. Older free plans had a limit of ten documents. Learn more about legacy limits.","title":"Number of documents"},{"location":"limits/#number-of-collaborators","text":"For team sites on all plans, there is no limit on the number of team members that may be added to the site. For paid plans, the number of team members determines the price. See our pricing page for details. Team members added to your team site may inherit access to workspaces or documents within that organization. Learn more about team sharing . On both personal and team sites, each document may be shared with up to 2 free guests who do not affect the plan price even on paid plans.","title":"Number of collaborators"},{"location":"limits/#number-of-tables-per-document","text":"There is a limit of 500 tables allowed per document. This is a soft limit. If you find yourself with a large number of tables, consider merging ones that have the same structure. For example, if you have a table for each product type, consider using a single table with the product type as an extra column.","title":"Number of tables per document"},{"location":"limits/#rows-per-document","text":"On the Free plan, documents have a limit of 5,000 rows. On the Pro plan, documents may have up to 100,000 rows. This is a rule of thumb. The actual limit depends also on the number of tables, columns, and the average size of data in each cell. One way to estimate it is to measure the size of the data when it is in CSV format: the limit is around 20MB in this format. For example, a document with 200,000 rows and 12 numeric columns would reach that. Attachments are counted separately. Attachments plus data in a single document are limited to 1GB on all plans.","title":"Rows per document"},{"location":"limits/#upload-limits","text":"Uploads are limited to 50MB, both for attachments and to import data. Note that an import within this limit may result in a document that exceeds the document size limit, in which case the upload is still likely to fail.","title":"Upload limits"},{"location":"limits/#api-limits","text":"Free plans are limited to 5,000 API calls per document per day. Pro plans raise the limit to 40,000 calls per document per day. Free plans may be rate limited to 5 API requests per second per document. The team plan does not impose such a rate limit. Separately, there is a concurrency limit of 10 for all plans: if 10 authorized API requests are currently being processed for a particular document, any other API requests will be rejected (with HTTP status code 429) until at least one of the original requests completes. A client that waits for one request to complete before sending the next would not hit this limit (assuming it is the sole client accessing the document). The size of the body of any individual API request is limited to 1MB. In particular, this means that requests adding or updating multiple records may need to be split into batches that fit within this limit.","title":"API limits"},{"location":"limits/#document-availability","text":"From time to time, during upgrades and operational transitions, individual Grist documents may become inaccessible for a period of some seconds. Please bear this in mind when using Grist\u2019s API.","title":"Document availability"},{"location":"limits/#legacy-limits","text":"Older free personal plans have the following limits: 10 documents per site No workspaces 100,000 rows To determine if you\u2019re on a legacy personal site, click on your site name (@your-name) in the top left. Personal sites on the legacy plan will say \u201cPersonal Site (Legacy)\u201d in the dropdown menu. On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page.","title":"Legacy limits"},{"location":"linking-widgets/","text":"Linking Page Widgets # One big reason for placing more than one widget on a page is that widgets can be linked. When linked, selecting a record in one widget will cause another widget to update and show only the data related to the selected record. For example, let\u2019s say you have a table of Departments in a company, and a table of Employees , with each employee tied to some department. You can have one Table widget listing departments and serving as a selector for a second Table widget listing employees: Previous Next To create this, first create a page with a Table widget for Departments data, as described in Page widgets . Then in the \u201cAdd New\u201d menu, select the \u201cAdd Widget to Page\u201d option to add another Table widget for Employees data. In the widget picker, use the \u201cSelect By\u201d dropdown and choose the \u201cDEPARTMENTS\u201d widget added in the first step. That\u2019s all it takes: now selecting a department in the first table will cause the second table to show only the employees in that department. Note that this relies on the Employees table having a column of type Reference with the target table of Departments . See Reference columns . Types of linking # Linking widgets only works when there is a relationship between records in the underlying data. There are several kinds of relationships supported. Same-record linking # Most directly you can link two widgets which show data from the same underlying table, usually linking a Table to a Card. This allows you to select a record in the Table widget and see more details of that record in the linked Card widget. For example, you can add a Card of Employees data and link it to an existing Table widget \u201cEMPLOYEES\u201d: When you select a record in the table, the new \u201cEMPLOYEES Card\u201d widget shows a card for the selected record. For another example of such linking, see the \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d and the Customizing Layout section in the associated tutorial. Filter linking # As in the Employee-Department example, when one table has a reference to another (i.e. a column of type \u201cReference\u201d), the second table can serve as a selector for the first. Essentially, selecting a record can automatically filter another widget to show only those records that refer to the selected record. In the example shown earlier, the Employees table has a \u201cReference\u201d column pointing to the Departments table, so a list of departments can serve as a selector for employees. When a department is selected, only the employees in that department will be shown. The \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d provides another example, where selecting a contact shows only the conversations with that contact. It\u2019s also described in the tutorial . When the target of linking is a Chart widget, you get dynamic charts that reflect data associated with the selected record. For example, you could link a pie chart to a department to show the sum of salaries for each job position in the selected department. Previous Next Indirect linking # Whenever a table A has a reference to B , A can be used in place of B as the source of linking. Instead of considering the currently selected record in A , linking will consider the referenced record in B as the selection. For instance, a Table widget showing Employees can serve as a selector for a Card widget showing data from Departments , using the fact that an employee record contains a \u201cDepartment\u201d reference. In the widget picker, if you select data from Departments , you\u2019ll see a \u201cSelect By\u201d option \u201cEMPLOYEES \u2022 Department\u201d: When you select an employee, you\u2019ll see the details of that employee\u2019s department. Multiple reference columns # When a table that\u2019s the target of linking has multiple reference columns, you may need to choose which one to use for linking. For instance, a Flight record might have fields \u2018DepartureAirport\u2019 and \u2018ArrivalAirport\u2019, each of which is a Reference to the table Airports . When you select an airport in a table, you can have a choice whether to show all flights departing from this airport or all flights arriving to it. The \u201cSelect By\u201d widget will show both options to choose from: Linking summary tables # When widgets display summarized data, as described in Summary tables , they can also be linked to the underlying data, data that references the underlying data, and to other summary tables. For example, you can summarize the table Employees by job position, and include the count of employees for each position, the average salary, or other summary data. You can also link the Employees table to it, so that selecting a position shows all employees in that position. Furthermore, you can link another summary table. For instance, you can summarize employees by position and gender, and link that to the summary by position. When selecting a job position, you can then see a summary by gender for that position. This is also convenient with charts. In this example, you see a pie chart with the average salary for men vs women for the selected job position. As you click different positions, the pie chart updates to reflect the selected one. More examples of such linking can be found in the Analyze and visualize tutorial. Lastly, tables that reference a summary table\u2019s underlying data may now be linked to the summary table itself. In the image below, the Champion Dog\u2019s table has a reference column to the Breeder table. The Breeder table is being summarized in the top right widget by the \u201cCountry\u201d column. Because Champion Dog references Breeder, you may add a widget of Champion Dogs that selects by a summary table of Breeder data. Changing link settings # After a widget is added, you can view and change its link settings from the right panel. One way to get to it is to click on the three-dots icon on the top right of the widget, and click on the \u201cData selection\u201d menu option: This opens the side panel, which shows what data is shown, and which widget, if any, serves as its selector. You can change the \u201cSelect By\u201d setting here, or click the green \u201cEdit Data Selection\u201d button, and change it in the widget picker dialog.","title":"Linking widgets"},{"location":"linking-widgets/#linking-page-widgets","text":"One big reason for placing more than one widget on a page is that widgets can be linked. When linked, selecting a record in one widget will cause another widget to update and show only the data related to the selected record. For example, let\u2019s say you have a table of Departments in a company, and a table of Employees , with each employee tied to some department. You can have one Table widget listing departments and serving as a selector for a second Table widget listing employees: Previous Next To create this, first create a page with a Table widget for Departments data, as described in Page widgets . Then in the \u201cAdd New\u201d menu, select the \u201cAdd Widget to Page\u201d option to add another Table widget for Employees data. In the widget picker, use the \u201cSelect By\u201d dropdown and choose the \u201cDEPARTMENTS\u201d widget added in the first step. That\u2019s all it takes: now selecting a department in the first table will cause the second table to show only the employees in that department. Note that this relies on the Employees table having a column of type Reference with the target table of Departments . See Reference columns .","title":"Linking Page Widgets"},{"location":"linking-widgets/#types-of-linking","text":"Linking widgets only works when there is a relationship between records in the underlying data. There are several kinds of relationships supported.","title":""},{"location":"linking-widgets/#same-record-linking","text":"Most directly you can link two widgets which show data from the same underlying table, usually linking a Table to a Card. This allows you to select a record in the Table widget and see more details of that record in the linked Card widget. For example, you can add a Card of Employees data and link it to an existing Table widget \u201cEMPLOYEES\u201d: When you select a record in the table, the new \u201cEMPLOYEES Card\u201d widget shows a card for the selected record. For another example of such linking, see the \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d and the Customizing Layout section in the associated tutorial.","title":"Same-record linking"},{"location":"linking-widgets/#filter-linking","text":"As in the Employee-Department example, when one table has a reference to another (i.e. a column of type \u201cReference\u201d), the second table can serve as a selector for the first. Essentially, selecting a record can automatically filter another widget to show only those records that refer to the selected record. In the example shown earlier, the Employees table has a \u201cReference\u201d column pointing to the Departments table, so a list of departments can serve as a selector for employees. When a department is selected, only the employees in that department will be shown. The \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d provides another example, where selecting a contact shows only the conversations with that contact. It\u2019s also described in the tutorial . When the target of linking is a Chart widget, you get dynamic charts that reflect data associated with the selected record. For example, you could link a pie chart to a department to show the sum of salaries for each job position in the selected department. Previous Next","title":"Filter linking"},{"location":"linking-widgets/#indirect-linking","text":"Whenever a table A has a reference to B , A can be used in place of B as the source of linking. Instead of considering the currently selected record in A , linking will consider the referenced record in B as the selection. For instance, a Table widget showing Employees can serve as a selector for a Card widget showing data from Departments , using the fact that an employee record contains a \u201cDepartment\u201d reference. In the widget picker, if you select data from Departments , you\u2019ll see a \u201cSelect By\u201d option \u201cEMPLOYEES \u2022 Department\u201d: When you select an employee, you\u2019ll see the details of that employee\u2019s department.","title":"Indirect linking"},{"location":"linking-widgets/#multiple-reference-columns","text":"When a table that\u2019s the target of linking has multiple reference columns, you may need to choose which one to use for linking. For instance, a Flight record might have fields \u2018DepartureAirport\u2019 and \u2018ArrivalAirport\u2019, each of which is a Reference to the table Airports . When you select an airport in a table, you can have a choice whether to show all flights departing from this airport or all flights arriving to it. The \u201cSelect By\u201d widget will show both options to choose from:","title":"Multiple reference columns"},{"location":"linking-widgets/#linking-summary-tables","text":"When widgets display summarized data, as described in Summary tables , they can also be linked to the underlying data, data that references the underlying data, and to other summary tables. For example, you can summarize the table Employees by job position, and include the count of employees for each position, the average salary, or other summary data. You can also link the Employees table to it, so that selecting a position shows all employees in that position. Furthermore, you can link another summary table. For instance, you can summarize employees by position and gender, and link that to the summary by position. When selecting a job position, you can then see a summary by gender for that position. This is also convenient with charts. In this example, you see a pie chart with the average salary for men vs women for the selected job position. As you click different positions, the pie chart updates to reflect the selected one. More examples of such linking can be found in the Analyze and visualize tutorial. Lastly, tables that reference a summary table\u2019s underlying data may now be linked to the summary table itself. In the image below, the Champion Dog\u2019s table has a reference column to the Breeder table. The Breeder table is being summarized in the top right widget by the \u201cCountry\u201d column. Because Champion Dog references Breeder, you may add a widget of Champion Dogs that selects by a summary table of Breeder data.","title":"Linking summary tables"},{"location":"linking-widgets/#changing-link-settings","text":"After a widget is added, you can view and change its link settings from the right panel. One way to get to it is to click on the three-dots icon on the top right of the widget, and click on the \u201cData selection\u201d menu option: This opens the side panel, which shows what data is shown, and which widget, if any, serves as its selector. You can change the \u201cSelect By\u201d setting here, or click the green \u201cEdit Data Selection\u201d button, and change it in the widget picker dialog.","title":"Changing link settings"},{"location":"newsletters/","text":"Grist for the Mill # Welcome to our monthly newsletter of updates and tips for Grist users. The phrase \u201cGrist for the mill\u201d means \u201cuseful experience, material, or knowledge\u201d, perfectly reflecting its purpose. To receive the newsletter, sign up for Grist . May 2024 Newsletter : New Grist Business plan, a formula timer utility, draggable conditionals and admin console improvements! April 2024 Newsletter : Filtering reference and choice dropdown lists, an admin console for self-hosters. March 2024 Newsletter : Improvements to forms, new file formats, and a boot page for self-hosters. February 2024 Newsletter : Community spotlight: new widgets, fun experiments, and in-depth explorations. January 2024 Newsletter : Grist Forms are here! Getting data into Grist just got easier. December 2023 Newsletter : Grist 2023 year in review, forms user testing and community showcase. November 2023 Newsletter : Open referenced records with a click, hang out with us on Discord, and add column types more easily. October 2023 Newsletter : New formula shortcuts, two experimental widgets, colorful calendar events and much more! September 2023 Newsletter : Calendar widget, two new templates, and API endpoint for making SQL queries. August 2023 Newsletter : Grist CSV Viewer! Llama AI support! July 2023 Newsletter : AI Formula Assistant launched! June 2023 Newsletter : Selector row highlighting, new templates, and community contributions. May 2023 Newsletter : Column and widget description, webhooks, and a vote for the best flashcards. April 2023 Newsletter : Flashcards contest, a way to prank friends with Grist, and a new experiment in publishing spreadsheet data. March 2023 Newsletter : Minimizing widgets, in-product tutorials, and a flashcards custom widget! February 2023 Newsletter : Grist in more languages, and a sneak peek into dev passion projects. January 2023 Newsletter : Grist en Fran\u00e7ais, Espa\u00f1ol, Portugu\u00eas, and Deutsch! Plus expanding widgets and access rules improvements. December 2022 Newsletter : New date filter with calendar, and snapshots in grist-core. November 2022 Newsletter : Grist experiment with AI formula writing, and improved sort and filter. October 2022 Newsletter : Quick sums, duplicate tables, and new API methods. September 2022 Newsletter : Dark Mode \ud83d\udd76 + improved user management. August 2022 Newsletter : Free team sites, conditional row styles, and better formula help. July 2022 Newsletter : Formula cheat sheet and summary tables in raw data. June 2022 Newsletter : Filtering by range, and new PEEK() function. May 2022 Newsletter : Raw data tables, and new summary table linking. April 2022 Newsletter : Rich text editor, and more cell styles. March 2022 Newsletter : Conditional formatting, new API method, and the new Sprouts Program. February 2022 Newsletter : Custom widgets menu, 2FA, access rules improvements, and cell context menu. January 2022 Newsletter : Managing template document tours, and 4 new templates, including an inventory manager. December 2021 Newsletter : Zapier instant triggers, and four new templates. November 2021 Newsletter : Import column mapping, filter on hidden columns, and more sorting options. October 2021 Newsletter : Editing choices, inline links, and enhanced import previews. September 2021 Newsletter : Improved imports, global currencies, and more ways to integrate. August 2021 Newsletter : Reference Lists, Embedding, Templates, Pabbly, and more. July 2021 Newsletter : Colors, Time and Authorship Stamps, and Google Sheets Integration. June 2021 Newsletter : Introducing Freeze Columns and a timesheet-tracking template. May 2021 Newsletter : Choice Lists, 3-step Tutorial on Reference Columns. April 2021 Newsletter : A 4-Step Guide to Link Keys. March 2021 Newsletter : Access Rules. February 2021 Newsletter : Improved Mobile Support, Totals. January 2021 Newsletter : Task Management, Find a Consultant, Be a Consultant. December 2020 Newsletter : Maps, Access Control, Compare Previous. November 2020 Newsletter : Open Source, Improved Attachments, Compare Versions. October 2020 Newsletter : Printing Support, Mailing Labels, Open Source Beta. September 2020 Newsletter : Public Access, Payroll Tracking. August 2020 Newsletter : Invoices, Custom Widgets, Document Lists. July 2020 Newsletter : Number Format Options, Prepare Emails with Formulas. June 2020 Newsletter : Work on Complex Changes, Automatic Backups. May 2020 Newsletter : Copy as Template, Better URLs, NY Tech Meetup.","title":"Newsletters"},{"location":"newsletters/#grist-for-the-mill","text":"Welcome to our monthly newsletter of updates and tips for Grist users. The phrase \u201cGrist for the mill\u201d means \u201cuseful experience, material, or knowledge\u201d, perfectly reflecting its purpose. To receive the newsletter, sign up for Grist . May 2024 Newsletter : New Grist Business plan, a formula timer utility, draggable conditionals and admin console improvements! April 2024 Newsletter : Filtering reference and choice dropdown lists, an admin console for self-hosters. March 2024 Newsletter : Improvements to forms, new file formats, and a boot page for self-hosters. February 2024 Newsletter : Community spotlight: new widgets, fun experiments, and in-depth explorations. January 2024 Newsletter : Grist Forms are here! Getting data into Grist just got easier. December 2023 Newsletter : Grist 2023 year in review, forms user testing and community showcase. November 2023 Newsletter : Open referenced records with a click, hang out with us on Discord, and add column types more easily. October 2023 Newsletter : New formula shortcuts, two experimental widgets, colorful calendar events and much more! September 2023 Newsletter : Calendar widget, two new templates, and API endpoint for making SQL queries. August 2023 Newsletter : Grist CSV Viewer! Llama AI support! July 2023 Newsletter : AI Formula Assistant launched! June 2023 Newsletter : Selector row highlighting, new templates, and community contributions. May 2023 Newsletter : Column and widget description, webhooks, and a vote for the best flashcards. April 2023 Newsletter : Flashcards contest, a way to prank friends with Grist, and a new experiment in publishing spreadsheet data. March 2023 Newsletter : Minimizing widgets, in-product tutorials, and a flashcards custom widget! February 2023 Newsletter : Grist in more languages, and a sneak peek into dev passion projects. January 2023 Newsletter : Grist en Fran\u00e7ais, Espa\u00f1ol, Portugu\u00eas, and Deutsch! Plus expanding widgets and access rules improvements. December 2022 Newsletter : New date filter with calendar, and snapshots in grist-core. November 2022 Newsletter : Grist experiment with AI formula writing, and improved sort and filter. October 2022 Newsletter : Quick sums, duplicate tables, and new API methods. September 2022 Newsletter : Dark Mode \ud83d\udd76 + improved user management. August 2022 Newsletter : Free team sites, conditional row styles, and better formula help. July 2022 Newsletter : Formula cheat sheet and summary tables in raw data. June 2022 Newsletter : Filtering by range, and new PEEK() function. May 2022 Newsletter : Raw data tables, and new summary table linking. April 2022 Newsletter : Rich text editor, and more cell styles. March 2022 Newsletter : Conditional formatting, new API method, and the new Sprouts Program. February 2022 Newsletter : Custom widgets menu, 2FA, access rules improvements, and cell context menu. January 2022 Newsletter : Managing template document tours, and 4 new templates, including an inventory manager. December 2021 Newsletter : Zapier instant triggers, and four new templates. November 2021 Newsletter : Import column mapping, filter on hidden columns, and more sorting options. October 2021 Newsletter : Editing choices, inline links, and enhanced import previews. September 2021 Newsletter : Improved imports, global currencies, and more ways to integrate. August 2021 Newsletter : Reference Lists, Embedding, Templates, Pabbly, and more. July 2021 Newsletter : Colors, Time and Authorship Stamps, and Google Sheets Integration. June 2021 Newsletter : Introducing Freeze Columns and a timesheet-tracking template. May 2021 Newsletter : Choice Lists, 3-step Tutorial on Reference Columns. April 2021 Newsletter : A 4-Step Guide to Link Keys. March 2021 Newsletter : Access Rules. February 2021 Newsletter : Improved Mobile Support, Totals. January 2021 Newsletter : Task Management, Find a Consultant, Be a Consultant. December 2020 Newsletter : Maps, Access Control, Compare Previous. November 2020 Newsletter : Open Source, Improved Attachments, Compare Versions. October 2020 Newsletter : Printing Support, Mailing Labels, Open Source Beta. September 2020 Newsletter : Public Access, Payroll Tracking. August 2020 Newsletter : Invoices, Custom Widgets, Document Lists. July 2020 Newsletter : Number Format Options, Prepare Emails with Formulas. June 2020 Newsletter : Work on Complex Changes, Automatic Backups. May 2020 Newsletter : Copy as Template, Better URLs, NY Tech Meetup.","title":"Grist for the Mill"},{"location":"on-demand-tables/","text":"On-Demand Tables # On-demand tables are an experimental feature The design of on-demand tables may change. For example, configuration options may be added, or aspects of the behavior of on-demand tables may be changed entirely. A defining feature of spreadsheets is the ability to update cells instantly as data they depend on changes. But sometimes a table is just a store of data, without any fancy calculations needed. In that case, you can choose to mark the table as \u201cOn-Demand\u201d in Grist. Grist can then serve data from that table faster, and make certain optimizations that may be helpful as the table gets large and regular tables become slow. When a table is marked \u201cOn-Demand\u201d: Data in the table will not generally be available for use in formulas. The table remains available for viewing and editing, but with caveats. Here\u2019s what you need to know about viewing data: Viewing is limited to 10000 rows at a time. Subsets of the table\u2019s rows can be selected by linking widgets , as for regular tables. You can expect good performance of linked widgets when the subset of the table\u2019s rows is less than 10000 rows, even if the full table is much larger. Here\u2019s what you need to know about editing data: You can edit data as normal in an On-Demand table. Automatic updates of anything that depends on that data simply won\u2019t happen. After edits, you need to reload the webpage to see everything updated. Here are some reasons you might make a table On-Demand: You want to make summaries and charts of slices of a large dataset using linked widgets . You are storing a lot of data in the table, and all you need to do with it is read parts of it back out via the API. Make an On-Demand Table # To convert a table to be an \u201cOn Demand\u201d table, open the right panel, pick the \u201cTable\u201d panel, and the \u201cData\u201d section. Click on \u201cAdvanced Settings\u201d and you should see a \u201cMake On-Demand\u201d button. If you change your mind, and don\u2019t want the table to be \u201cOn-Demand\u201d anymore, you can find a button to undo this setting in the same place: Changing a table to become \u201cOn-Demand\u201d or to stop being \u201cOn-Demand\u201d will force the document to reload for all users viewing it at that moment. Formulas, References and On-Demand Tables # In general, formulas and on-demand tables don\u2019t go together. That said, if you\u2019re careful you can use the following very simple formulas: $column - where the column mentioned is not itself a formula. This copies data from another column verbatim. $reference.column - where reference is a reference column , and column is not itself a formula. This formula support is enough to unlock Grist\u2019s linking widgets feature, which is why it is present. In general, if you try using formulas and On-Demand tables, you are setting yourself up for sadness. Remember, like any edit of an On-Demand table, when you add or change a formula column you\u2019ll generally need to reload to see cell values updated. Some type conversions, such as converting a column to be a reference, are not effective for On-Demand tables. So it is important to perform such conversions before making a table On-Demand. From formulas in regular tables, you cannot access the content of on-demand tables.","title":"On-Demand tables"},{"location":"on-demand-tables/#on-demand-tables","text":"On-demand tables are an experimental feature The design of on-demand tables may change. For example, configuration options may be added, or aspects of the behavior of on-demand tables may be changed entirely. A defining feature of spreadsheets is the ability to update cells instantly as data they depend on changes. But sometimes a table is just a store of data, without any fancy calculations needed. In that case, you can choose to mark the table as \u201cOn-Demand\u201d in Grist. Grist can then serve data from that table faster, and make certain optimizations that may be helpful as the table gets large and regular tables become slow. When a table is marked \u201cOn-Demand\u201d: Data in the table will not generally be available for use in formulas. The table remains available for viewing and editing, but with caveats. Here\u2019s what you need to know about viewing data: Viewing is limited to 10000 rows at a time. Subsets of the table\u2019s rows can be selected by linking widgets , as for regular tables. You can expect good performance of linked widgets when the subset of the table\u2019s rows is less than 10000 rows, even if the full table is much larger. Here\u2019s what you need to know about editing data: You can edit data as normal in an On-Demand table. Automatic updates of anything that depends on that data simply won\u2019t happen. After edits, you need to reload the webpage to see everything updated. Here are some reasons you might make a table On-Demand: You want to make summaries and charts of slices of a large dataset using linked widgets . You are storing a lot of data in the table, and all you need to do with it is read parts of it back out via the API.","title":"On-Demand Tables"},{"location":"on-demand-tables/#make-an-on-demand-table","text":"To convert a table to be an \u201cOn Demand\u201d table, open the right panel, pick the \u201cTable\u201d panel, and the \u201cData\u201d section. Click on \u201cAdvanced Settings\u201d and you should see a \u201cMake On-Demand\u201d button. If you change your mind, and don\u2019t want the table to be \u201cOn-Demand\u201d anymore, you can find a button to undo this setting in the same place: Changing a table to become \u201cOn-Demand\u201d or to stop being \u201cOn-Demand\u201d will force the document to reload for all users viewing it at that moment.","title":"Make an On-Demand Table"},{"location":"on-demand-tables/#formulas-references-and-on-demand-tables","text":"In general, formulas and on-demand tables don\u2019t go together. That said, if you\u2019re careful you can use the following very simple formulas: $column - where the column mentioned is not itself a formula. This copies data from another column verbatim. $reference.column - where reference is a reference column , and column is not itself a formula. This formula support is enough to unlock Grist\u2019s linking widgets feature, which is why it is present. In general, if you try using formulas and On-Demand tables, you are setting yourself up for sadness. Remember, like any edit of an On-Demand table, when you add or change a formula column you\u2019ll generally need to reload to see cell values updated. Some type conversions, such as converting a column to be a reference, are not effective for On-Demand tables. So it is important to perform such conversions before making a table On-Demand. From formulas in regular tables, you cannot access the content of on-demand tables.","title":"Formulas, References and On-Demand Tables"},{"location":"page-widgets/","text":"Pages & widgets # Unlike traditional spreadsheets, in Grist you can create multiple views of the same data, and display multiple data sets on one page. This allows you to create useful dashboards and custom applications tailored to your needs. Pages # In Grist, you organize your document into \u201cpages\u201d. These are listed in the left panel, with collapsible groups. You may rearrange and group pages in the left panel by dragging them to suit your needs. You can rename, remove, or duplicate pages using the three-dots menu next to the page name in the list. Renaming the page does not edit data tables\u2019 names or widget titles. See changing widget below to learn how to edit table and widget names. Duplicating a page duplicates views of data and does not duplicate the data itself. Removing a page does not delete data. When removing the last view of data, you will be asked if you want to delete only the view, but not the data itself; or if you want to delete both the page and the underlying data table(s). Learn more about your document\u2019s data in the raw data page . Note that pages can also be renamed by clicking the page name on top of the screen. Using the opener icon ( ) near the top of the left panel, you can collapse the panel to show only the initials of each page, leaving more screen space to view your data. To add a new page, use the \u201cAdd New\u201d button, and click \u201cAdd Page\u201d. At that point, you\u2019ll get to choose the page widget to include in the new page. Using Emojis in Page and Widget Names You can add any emoji to a Page or Widget name. The keyboard shortcut to open the emoji keyboard is Windows Logo + . (period) on PC or Command + Control + Space on Mac. You can also copy/paste an online source like Emojipedia . When a Page name starts with an emoji, it will replace the page icon. Page widgets # A page contains sections, such as tables or charts, which we call \u201cpage widgets\u201d. Each page widget shows data from one table. A page may contain more than one page widget, and you can arrange and link them to create useful layouts. Here are the supported kinds of page widgets. The salient features of each one are described on separate pages. Table : similar to the spreadsheet grid and a good way to see many records at once. Card : shows a single record in a form-like layout which you can customize. Card List : uses the same layout options as a card, displays a scrollable list of records. Chart : plots data on a chart with support for several different chart types. Calendar : displays event data in a calendar view. Custom : inserts a custom webpage, optionally granting it access to the document\u2019s data. There is a special page called raw data that lists all data tables in your document and summarizes your document\u2019s usage statistics. Navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu. Widget picker # The menu opened by the \u201cAdd New\u201d button has options \u201cAdd Page\u201d and \u201cAdd Widget to Page\u201d. In either case, you\u2019ll see the \u201cpage widget picker\u201d where you can choose your desired widget: You can select the type of widget and the table of data to show (or \u201cNew Table\u201d to create a new table). The \u201csummary\u201d icon ( ) allows you to summarize data . When adding a widget to an existing page, you\u2019ll also see a \u201cSelect By\u201d option, which allows linking this widget to another one already on the page. This process is described in greater detail in Linking widgets . Once you\u2019ve added widgets, they can be moved around and resized, as described in Custom Layouts . Changing widget or its data # If you\u2019d like to change a widget or the data it displays after it\u2019s added, you may do so. Click the three-dots button on the top right of your widget, and select \u201cWidget options\u201d. This opens the right-side panel. Click on \u201cChange Widget\u201d. You can then use the widget picker to change the widget or the data it displays. You may also edit the widget\u2019s title or add a description. Renaming widgets # You can rename widgets in several ways. We saw in the section above that you can edit a widget\u2019s title or add a description from the Widget tab of the Creator Panel. Another way is to click on the widget title above a widget. From here, you can edit the widget\u2019s title, the underlying data table\u2019s name or add a description. By default, the widget title is the data table\u2019s name. To override this, enter a new title under \u2018Widget Title\u2019. Learn more about data tables in the raw data page . Configuring field lists # Although different kinds of page widgets look very different, they all represent a list of records. Any of the widget types can be used to show the same underlying data. In a Table , each record is represented by a row, and columns represent the same kind of value for each record. Note that the raw data page lists all data tables. In a Card List , each row of the underlying data is shown as a card. Each column in the data corresponds to a field in this card. When talking about a Card widget, we\u2019ll use the term \u201cfield\u201d , which conceptually is the same as a \u201ccolumn\u201d in a Table widget. A Card is just like a Card List, but shows only one row of data at a time. In a Chart , each row of the underlying data table becomes a graphical element, such as a point on a line chart, a bar in a bar chart, or a slice of a pie chart. In this context, the columns of our data table are better known as data \u201cseries\u201d . Click on the opener icon ( ) to open the right panel. Depending on the currently-selected widget, you might see a tab for configuring a Column, Field, or Series. These are not different in substance, but different terms make more sense for different widgets. Clicking on the widget tab (highlighted in green in the images above), you\u2019ll see subtabs for \u201cWidget\u201d, \u201cSort & Filter\u201d, and \u201cData\u201d. We\u2019ll focus on the first one: \u201cWidget\u201d. You\u2019ll see options specific to the type of the selected widget, and below that two lists: \u201cVisible Columns\u201d and \u201cHidden Columns\u201d. The \u201cHidden Columns\u201d are the columns available in the data, but not shown in this widget. In a Card, these lists would show up as \u201cVisible Fields\u201d / \u201cHidden Fields\u201d. In a chart, they show up as \u201cVisible Series\u201d / \u201cHidden Series\u201d. These lists allow you to include, exclude, or rearrange fields in a widget. As you move your mouse over the items in the list, use the \u201ceye\u201d icons that pop up to show or hide them. Alternatively, you can select several items using the checkboxes, and hide or show them together. This ordered list of fields can be used to customize any of the page widget types. It has a particular importance in the Chart widget , where different chart types and options require you to place series in a certain order in the \u201cVisible Series\u201d list to ensure your data is plotted correctly.","title":"Pages & widgets"},{"location":"page-widgets/#pages-widgets","text":"Unlike traditional spreadsheets, in Grist you can create multiple views of the same data, and display multiple data sets on one page. This allows you to create useful dashboards and custom applications tailored to your needs.","title":""},{"location":"page-widgets/#pages","text":"In Grist, you organize your document into \u201cpages\u201d. These are listed in the left panel, with collapsible groups. You may rearrange and group pages in the left panel by dragging them to suit your needs. You can rename, remove, or duplicate pages using the three-dots menu next to the page name in the list. Renaming the page does not edit data tables\u2019 names or widget titles. See changing widget below to learn how to edit table and widget names. Duplicating a page duplicates views of data and does not duplicate the data itself. Removing a page does not delete data. When removing the last view of data, you will be asked if you want to delete only the view, but not the data itself; or if you want to delete both the page and the underlying data table(s). Learn more about your document\u2019s data in the raw data page . Note that pages can also be renamed by clicking the page name on top of the screen. Using the opener icon ( ) near the top of the left panel, you can collapse the panel to show only the initials of each page, leaving more screen space to view your data. To add a new page, use the \u201cAdd New\u201d button, and click \u201cAdd Page\u201d. At that point, you\u2019ll get to choose the page widget to include in the new page. Using Emojis in Page and Widget Names You can add any emoji to a Page or Widget name. The keyboard shortcut to open the emoji keyboard is Windows Logo + . (period) on PC or Command + Control + Space on Mac. You can also copy/paste an online source like Emojipedia . When a Page name starts with an emoji, it will replace the page icon.","title":"Pages"},{"location":"page-widgets/#page-widgets","text":"A page contains sections, such as tables or charts, which we call \u201cpage widgets\u201d. Each page widget shows data from one table. A page may contain more than one page widget, and you can arrange and link them to create useful layouts. Here are the supported kinds of page widgets. The salient features of each one are described on separate pages. Table : similar to the spreadsheet grid and a good way to see many records at once. Card : shows a single record in a form-like layout which you can customize. Card List : uses the same layout options as a card, displays a scrollable list of records. Chart : plots data on a chart with support for several different chart types. Calendar : displays event data in a calendar view. Custom : inserts a custom webpage, optionally granting it access to the document\u2019s data. There is a special page called raw data that lists all data tables in your document and summarizes your document\u2019s usage statistics. Navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu.","title":"Page widgets"},{"location":"page-widgets/#widget-picker","text":"The menu opened by the \u201cAdd New\u201d button has options \u201cAdd Page\u201d and \u201cAdd Widget to Page\u201d. In either case, you\u2019ll see the \u201cpage widget picker\u201d where you can choose your desired widget: You can select the type of widget and the table of data to show (or \u201cNew Table\u201d to create a new table). The \u201csummary\u201d icon ( ) allows you to summarize data . When adding a widget to an existing page, you\u2019ll also see a \u201cSelect By\u201d option, which allows linking this widget to another one already on the page. This process is described in greater detail in Linking widgets . Once you\u2019ve added widgets, they can be moved around and resized, as described in Custom Layouts .","title":"Widget picker"},{"location":"page-widgets/#changing-widget-or-its-data","text":"If you\u2019d like to change a widget or the data it displays after it\u2019s added, you may do so. Click the three-dots button on the top right of your widget, and select \u201cWidget options\u201d. This opens the right-side panel. Click on \u201cChange Widget\u201d. You can then use the widget picker to change the widget or the data it displays. You may also edit the widget\u2019s title or add a description.","title":"Changing widget or its data"},{"location":"page-widgets/#renaming-widgets","text":"You can rename widgets in several ways. We saw in the section above that you can edit a widget\u2019s title or add a description from the Widget tab of the Creator Panel. Another way is to click on the widget title above a widget. From here, you can edit the widget\u2019s title, the underlying data table\u2019s name or add a description. By default, the widget title is the data table\u2019s name. To override this, enter a new title under \u2018Widget Title\u2019. Learn more about data tables in the raw data page .","title":"Renaming widgets"},{"location":"page-widgets/#configuring-field-lists","text":"Although different kinds of page widgets look very different, they all represent a list of records. Any of the widget types can be used to show the same underlying data. In a Table , each record is represented by a row, and columns represent the same kind of value for each record. Note that the raw data page lists all data tables. In a Card List , each row of the underlying data is shown as a card. Each column in the data corresponds to a field in this card. When talking about a Card widget, we\u2019ll use the term \u201cfield\u201d , which conceptually is the same as a \u201ccolumn\u201d in a Table widget. A Card is just like a Card List, but shows only one row of data at a time. In a Chart , each row of the underlying data table becomes a graphical element, such as a point on a line chart, a bar in a bar chart, or a slice of a pie chart. In this context, the columns of our data table are better known as data \u201cseries\u201d . Click on the opener icon ( ) to open the right panel. Depending on the currently-selected widget, you might see a tab for configuring a Column, Field, or Series. These are not different in substance, but different terms make more sense for different widgets. Clicking on the widget tab (highlighted in green in the images above), you\u2019ll see subtabs for \u201cWidget\u201d, \u201cSort & Filter\u201d, and \u201cData\u201d. We\u2019ll focus on the first one: \u201cWidget\u201d. You\u2019ll see options specific to the type of the selected widget, and below that two lists: \u201cVisible Columns\u201d and \u201cHidden Columns\u201d. The \u201cHidden Columns\u201d are the columns available in the data, but not shown in this widget. In a Card, these lists would show up as \u201cVisible Fields\u201d / \u201cHidden Fields\u201d. In a chart, they show up as \u201cVisible Series\u201d / \u201cHidden Series\u201d. These lists allow you to include, exclude, or rearrange fields in a widget. As you move your mouse over the items in the list, use the \u201ceye\u201d icons that pop up to show or hide them. Alternatively, you can select several items using the checkboxes, and hide or show them together. This ordered list of fields can be used to customize any of the page widget types. It has a particular importance in the Chart widget , where different chart types and options require you to place series in a certain order in the \u201cVisible Series\u201d list to ensure your data is plotted correctly.","title":"Configuring field lists"},{"location":"python/","text":"Python # Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Python formulas are evaluated in a sandbox, without internet access, and without a persistent filesystem. Supported Python versions # We currently support two versions of Python: Python 3 (specifically 3.11 at the time of writing) Python 2 (specifically 2.7) Newly created documents on our hosted service use Python 3 by default, while older documents (created before November 2021 approximately) use Python 2 by default. To tell which version of Python a specific document uses, look at its Document Settings . The Engine setting may be python2 , python3 , or blank. A blank setting implies python2 . If you have editing rights on a document, you can change the Engine setting, and the document will then reload with all formulas now interpreted using the version of Python you have specified. We recommend caution in doing so. A formula that works as intended in one version of Python may give errors in another, or (worse) give the wrong results. Warning Some formulas may fail or give wrong results if used with a version of Python that is different from the one for which they were written. Python 2 reached its end of life in January 2020, so if you look online for python help, the answers you find are more and more likely to be for Python 3. If you have a document that uses Python 2, and you\u2019d like to switch it to use Python 3, we recommend reading Testing the effect of changing Python versions and Differences between Python versions . Be sure to check all tables and columns, and both regular formalas and trigger formulas. We\u2019d be interested to hear your experience, and to help with any problems, on the community forum . Self-hosted Grist may use any version of Python you configure it with, but bear in mind we actively test only the supported versions. Testing the effect of changing Python versions # Grist has some features that can help you evaluate the consequences of changing the Python version a document uses. The Work on a Copy feature is useful to experiment with changing the Python version without affecting your document until you are ready. There is a \u201cCompare with original\u201d option that will let you visualize which cells changed, if any. Be sure to look at all tables and columns. The Activity tab of Document History (with \u201cAll Tables\u201d selected) lets you review in more detail what has changed. Be careful to test any trigger formulas you may have, since the Python code in them won\u2019t be exercised until you specifically trigger these formulas. You can use the code viewer to quickly remind yourself of all formulas in a document, so you can systematically check them all. Differences between Python versions # There are important differences between Python 2 and 3. Formulas may need to be changed in order to give the same results when switching between Python versions. There are many online resources such as this compatibility cheatsheet which can help figure out what the issue is when you hit a difference, and get ideas on how to resolve it. Here, we list common cases we\u2019ve seen in Grist formulas. Division of whole numbers # In Python 2, dividing whole numbers gives a whole number, so 9 / 2 is 4 . In Python 3, it is 4.5 . For a spreadsheet, this is a much more sensible answer, but if you rely on the Python 2 behavior, we suggest you switch to the // operator which is consistent between versions ( 9 // 2 is 4 for both). For example the General Ledger template had a Python 2 formula for computing the quarter from a date (so a Date of 2021-08-15 gave a Quarter of 2021 Q3 ) as follows: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) / 3) when switching to Python 3, this needed correcting to: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) // 3) Otherwise Quarters became fractional! Some imports are reorganized # Python has a useful standard library, but some parts of it were moved around between Python 2 and 3. For example, several of our templates have formulas to construct URLs, to open custom searches for example, or to open a pre-populated email with calculated To , CC , and Subject values. Python has handy helpers for constructing URLs, but they moved around a bit between Python versions. Our Lightweight CRM example had a Python 2 formula like this to kick off a search for emails in Gmail: from urllib import quote_plus \"Gmail search https://mail.google.com/mail/u/0/#search/%s\" % quote_plus($Email) In Python 3, the import line needed changing to: from urllib.parse import quote_plus Subtle change in rounding # Python 3 switches the built-in function round() from rounding the way many people learned in school (where when rounding .5 , you always round up) to what is called \u201cbanker\u2019s rounding\u201d (where you round from .5 to the nearest even number). This is generally accepted as an improvement, mitigating a bias to larger numbers that can become significant at scale. But it could be a surprise to see numbers change like this in an established document. If you really need Python 2\u2019s rounding, replace any calls to Python\u2019s round function with the Excel-compatible ROUND function. For example: round($val, 2) would be replaced with: ROUND($val, 2) Unicode text handling # Python 2 does not shine at handling international text and emojis. We have mitigated many problems by setting the default encoding to utf8 for all documents. Nevertheless, when switching from Python 2 to Python 3, you may see type changes or errors. Consider this Python 2 formula to generate a one-way hash of an email address: import hashlib hashlib.sha256($Email).hexdigest() In Python 3 this fails with TypeError: Unicode-objects must be encoded before hashing , which can be resolved by replacing $Email with $Email.encode() : import hashlib hashlib.sha256($Email.encode()).hexdigest()","title":"Python versions"},{"location":"python/#python","text":"Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Python formulas are evaluated in a sandbox, without internet access, and without a persistent filesystem.","title":"Python"},{"location":"python/#supported-python-versions","text":"We currently support two versions of Python: Python 3 (specifically 3.11 at the time of writing) Python 2 (specifically 2.7) Newly created documents on our hosted service use Python 3 by default, while older documents (created before November 2021 approximately) use Python 2 by default. To tell which version of Python a specific document uses, look at its Document Settings . The Engine setting may be python2 , python3 , or blank. A blank setting implies python2 . If you have editing rights on a document, you can change the Engine setting, and the document will then reload with all formulas now interpreted using the version of Python you have specified. We recommend caution in doing so. A formula that works as intended in one version of Python may give errors in another, or (worse) give the wrong results. Warning Some formulas may fail or give wrong results if used with a version of Python that is different from the one for which they were written. Python 2 reached its end of life in January 2020, so if you look online for python help, the answers you find are more and more likely to be for Python 3. If you have a document that uses Python 2, and you\u2019d like to switch it to use Python 3, we recommend reading Testing the effect of changing Python versions and Differences between Python versions . Be sure to check all tables and columns, and both regular formalas and trigger formulas. We\u2019d be interested to hear your experience, and to help with any problems, on the community forum . Self-hosted Grist may use any version of Python you configure it with, but bear in mind we actively test only the supported versions.","title":"Supported Python versions"},{"location":"python/#testing-the-effect-of-changing-python-versions","text":"Grist has some features that can help you evaluate the consequences of changing the Python version a document uses. The Work on a Copy feature is useful to experiment with changing the Python version without affecting your document until you are ready. There is a \u201cCompare with original\u201d option that will let you visualize which cells changed, if any. Be sure to look at all tables and columns. The Activity tab of Document History (with \u201cAll Tables\u201d selected) lets you review in more detail what has changed. Be careful to test any trigger formulas you may have, since the Python code in them won\u2019t be exercised until you specifically trigger these formulas. You can use the code viewer to quickly remind yourself of all formulas in a document, so you can systematically check them all.","title":"Testing the effect of changing Python versions"},{"location":"python/#differences-between-python-versions","text":"There are important differences between Python 2 and 3. Formulas may need to be changed in order to give the same results when switching between Python versions. There are many online resources such as this compatibility cheatsheet which can help figure out what the issue is when you hit a difference, and get ideas on how to resolve it. Here, we list common cases we\u2019ve seen in Grist formulas.","title":"Differences between Python versions"},{"location":"python/#division-of-whole-numbers","text":"In Python 2, dividing whole numbers gives a whole number, so 9 / 2 is 4 . In Python 3, it is 4.5 . For a spreadsheet, this is a much more sensible answer, but if you rely on the Python 2 behavior, we suggest you switch to the // operator which is consistent between versions ( 9 // 2 is 4 for both). For example the General Ledger template had a Python 2 formula for computing the quarter from a date (so a Date of 2021-08-15 gave a Quarter of 2021 Q3 ) as follows: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) / 3) when switching to Python 3, this needed correcting to: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) // 3) Otherwise Quarters became fractional!","title":"Division of whole numbers"},{"location":"python/#some-imports-are-reorganized","text":"Python has a useful standard library, but some parts of it were moved around between Python 2 and 3. For example, several of our templates have formulas to construct URLs, to open custom searches for example, or to open a pre-populated email with calculated To , CC , and Subject values. Python has handy helpers for constructing URLs, but they moved around a bit between Python versions. Our Lightweight CRM example had a Python 2 formula like this to kick off a search for emails in Gmail: from urllib import quote_plus \"Gmail search https://mail.google.com/mail/u/0/#search/%s\" % quote_plus($Email) In Python 3, the import line needed changing to: from urllib.parse import quote_plus","title":"Some imports are reorganized"},{"location":"python/#subtle-change-in-rounding","text":"Python 3 switches the built-in function round() from rounding the way many people learned in school (where when rounding .5 , you always round up) to what is called \u201cbanker\u2019s rounding\u201d (where you round from .5 to the nearest even number). This is generally accepted as an improvement, mitigating a bias to larger numbers that can become significant at scale. But it could be a surprise to see numbers change like this in an established document. If you really need Python 2\u2019s rounding, replace any calls to Python\u2019s round function with the Excel-compatible ROUND function. For example: round($val, 2) would be replaced with: ROUND($val, 2)","title":"Subtle change in rounding"},{"location":"python/#unicode-text-handling","text":"Python 2 does not shine at handling international text and emojis. We have mitigated many problems by setting the default encoding to utf8 for all documents. Nevertheless, when switching from Python 2 to Python 3, you may see type changes or errors. Consider this Python 2 formula to generate a one-way hash of an email address: import hashlib hashlib.sha256($Email).hexdigest() In Python 3 this fails with TypeError: Unicode-objects must be encoded before hashing , which can be resolved by replacing $Email with $Email.encode() : import hashlib hashlib.sha256($Email.encode()).hexdigest()","title":"Unicode text handling"},{"location":"raw-data/","text":"Raw data page # The raw data page is a special page that lists all data tables in your document and summarizes your document\u2019s usage statistics. From your document, navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu. Unlike other pages , the layout in the raw data page cannot be customized. From the list of data tables, you can find the data table\u2019s name and id, and remove data. Note that removing a data table from this page will delete data and remove it from all pages. This is different from other pages where it is possible to remove a view of data and not delete the data itself. Click on a data table to open it. Note that in the creator panel the widget type cannot be changed. Renaming the widget also renames the data table. Because raw data is intended to show all data, columns cannot be hidden, either. However, columns can be rearranged, deleted, created, and modified. For creators, this view may make it easier to edit data structure, add formulas , conditional formatting , and so on. Duplicating Data # Tables can be duplicated from the Raw Data page. Click the three-dot icon to the right of the table you wish to duplicate then select \u2018Duplicate Table\u2019 from the menu. By default, a duplicated table will only contain the table structure, not the data. If you wish to copy all data in addition to the table structure, be sure to check the box prior to clicking \u2018Save\u2019. If the original table has any access rules, those rules will not be duplicated. Only the document\u2019s default rules will apply to the copied table. The duplicate table is a new table that is not linked to the original. Meaning, if you update data in the copy, the original table will not be updated, and vice versa. Note that instead of duplicating tables, it\u2019s usually better to segment data by adding a new column. Let\u2019s use expenses as an example. Rather than having separate tables for each month\u2019s expenses, it is better to include all data in a single table and create a new column called \u201cMonth\u201d to segment rows into months. In general, if you have multiple tables with near identical columns, this is an indicator that the data could all be in the same table. Doing so may make data analysis easier. Usage # Usage statistics are summarized beneath the list of data tables. Note that usage applies to the entire document, not individual tables. Learn more about document limits .","title":"Raw data page"},{"location":"raw-data/#raw-data-page","text":"The raw data page is a special page that lists all data tables in your document and summarizes your document\u2019s usage statistics. From your document, navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu. Unlike other pages , the layout in the raw data page cannot be customized. From the list of data tables, you can find the data table\u2019s name and id, and remove data. Note that removing a data table from this page will delete data and remove it from all pages. This is different from other pages where it is possible to remove a view of data and not delete the data itself. Click on a data table to open it. Note that in the creator panel the widget type cannot be changed. Renaming the widget also renames the data table. Because raw data is intended to show all data, columns cannot be hidden, either. However, columns can be rearranged, deleted, created, and modified. For creators, this view may make it easier to edit data structure, add formulas , conditional formatting , and so on.","title":"Raw data page"},{"location":"raw-data/#duplicating-data","text":"Tables can be duplicated from the Raw Data page. Click the three-dot icon to the right of the table you wish to duplicate then select \u2018Duplicate Table\u2019 from the menu. By default, a duplicated table will only contain the table structure, not the data. If you wish to copy all data in addition to the table structure, be sure to check the box prior to clicking \u2018Save\u2019. If the original table has any access rules, those rules will not be duplicated. Only the document\u2019s default rules will apply to the copied table. The duplicate table is a new table that is not linked to the original. Meaning, if you update data in the copy, the original table will not be updated, and vice versa. Note that instead of duplicating tables, it\u2019s usually better to segment data by adding a new column. Let\u2019s use expenses as an example. Rather than having separate tables for each month\u2019s expenses, it is better to include all data in a single table and create a new column called \u201cMonth\u201d to segment rows into months. In general, if you have multiple tables with near identical columns, this is an indicator that the data could all be in the same table. Doing so may make data analysis easier.","title":"Duplicating Data"},{"location":"raw-data/#usage","text":"Usage statistics are summarized beneath the list of data tables. Note that usage applies to the entire document, not individual tables. Learn more about document limits .","title":"Usage"},{"location":"record-cards/","text":"Record Cards # Record Cards are a quick, easy way to view a record\u2019s details. To view a record\u2019s details as a card, hover over the row number and click the arrow icon that appears. Select \u2018View as card\u2019 from the dropdown menu. This opens an editable card view of the record\u2019s details. If your table contains a reference or reference list column , you can click the link icon to open the linked record\u2019s card. A record card will open to display record data for the referenced record. Editing a Record Card\u2019s Layout # You can edit a record card\u2019s layout from the Raw Data page. Click the card icon to open. You can drag and drop fields to rearrange, resize and add/delete fields from the view. Learn more about editing card layouts . Disabling a Record Card # You can also disable a record card from the Raw Data page. To disable a record card for a particular table, click the three-dot icon to the right of the table name then select \u2018Disable Record Card\u2019 from the dropdown.","title":"Record Cards"},{"location":"record-cards/#record-cards","text":"Record Cards are a quick, easy way to view a record\u2019s details. To view a record\u2019s details as a card, hover over the row number and click the arrow icon that appears. Select \u2018View as card\u2019 from the dropdown menu. This opens an editable card view of the record\u2019s details. If your table contains a reference or reference list column , you can click the link icon to open the linked record\u2019s card. A record card will open to display record data for the referenced record.","title":"Record Cards"},{"location":"record-cards/#editing-a-record-cards-layout","text":"You can edit a record card\u2019s layout from the Raw Data page. Click the card icon to open. You can drag and drop fields to rearrange, resize and add/delete fields from the view. Learn more about editing card layouts .","title":"Editing a Record Card's Layout"},{"location":"record-cards/#disabling-a-record-card","text":"You can also disable a record card from the Raw Data page. To disable a record card for a particular table, click the three-dot icon to the right of the table name then select \u2018Disable Record Card\u2019 from the dropdown.","title":"Disabling a Record Card"},{"location":"references-lookups/","text":"Using References and Lookups in Formulas # Reference and Reference List columns in Grist allow one table to create an explicit reference to another. A common example of this is seen in the Class Enrollment template. On the Staff page, we have a list of staff members. On the classes page, we have a reference column labeled Instructor that references the records on our Staff page. Keep in mind, it\u2019s not just referencing the Full Name column but the entire record associated with the selected instructor. Reference columns and dot notation # Using a Reference column within a formula can make it easy to get any data from the referenced record. To do this, we use dot notation. It uses the format $A.B where A is the name of the reference column and B is the name of the column in the referenced table that we want to pull data from. Let\u2019s see this in action on the Enrollment View page of the Class Enrollment template. Dot notation is used in the Class_Times column of the ENROLLMENTS table, found at the bottom right of the Enrollment View page. We can see that the Class_Times column is using a formula with dot notation. Using the format $A.B described above, we can figure out that Class is the name of the reference column and Times is the name of the column in the referenced table. Let\u2019s track this back to where it\u2019s pulling from - since the reference column is Class, we can look at that column\u2019s information to find out what table it is pulling from. The Class column references data from the Classes table. Therefore, the Class_Times column is pulling from the Times column of the Classes table. Chaining # If the reference lookup returns a reference, this can be chained. Perhaps we want to add the Instructor\u2019s phone number to the Enrollments table. We can use the Class reference column to pull the instructor\u2019s information from the Classes table. As you can see in the screenshot above, the instructor column is a reference column itself. If we follow the format from before, our dot notation would be $Class.Instructor but the Instructor column points to the entire record of the instructor so we need to tell it what information we want from this record, creating a chain. The instructor column references the Staff table so we navigate there to find out what column we need to pull information from in order to get the phone number. The column that contains the instructor\u2019s phone number is Phone. Putting this all together, our dot notation for the instructor\u2019s phone number would be $Class.Instructor.Phone What happens if we leave our formulas as $Class.Instructor ? You will see a numeric record ID of the record in the Staff table that the Instructor column points to. That\u2019s what a reference column really stores. If you change the type of this formula column to Reference, you will be able to select a column to show, such as the Full Name. Another way to see the name is to chain the dot-notation, as we did for phone: $Class.Instructor.Full_Name . lookupOne # Another way to point to a record is using Table.lookupOne(...) function. lookupOne allows you to look up a record by some fields, similar to Excel\u2019s VLOOKUP. In fact, Grist\u2019s version of VLOOKUP is merely an alias for lookupOne. lookupOne is rarely useful in Grist, because using a Reference type column is usually the preferred solution to connect records. However, on some occasions, lookupOne can be useful. One situation is when you have two sets of data which overlap even though they represent something different and perhaps come from different sources. An example of this can be found in our Event Sponsors + Attendees (References and Lookups) document which is a modified version of the Event Sponsors + Attendees template, available in our template gallery . Let\u2019s say that you run an event and have a list of registered attendees, as well as Sponsors. Registered attendees are stored in the All Registrations table, perhaps populated via a form integration. Sponsors are listed in a separate table, with fields related to their sponsorship, and perhaps maintained by another team. Both tables contain email addresses which identify attendees and sponsors. Sometimes a sponsor may register to attend the event. In that case, you\u2019ll have an Attendee record with an email address that also appears in the Sponsors table. That\u2019s useful to know for someone looking at the attendee list. You can lookup a record in the sponsors table by email address by using a lookupOne formula. The Sponsor column in the All Registrations table does just that using this formula: Sponsors.lookupOne(Contact_Email=$Registration_Email) This formula is looking to see if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. The general format for a lookupOne formula is: [Table_Name].lookupOne([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Such a formula returns a reference. In the screenshot above, you can see the lookup result returns Sponsors[#] . The number it returns between square brackets is the record ID of the lookup result. Where it returns Sponsors[0] , no match was found. It\u2019s often a good idea to create a column for the lookup result and change its type to Reference, as you see in the screenshot below. Then, if there is a match, the reference column will point to the entire matched record. Like any reference column, you can select which field from that record to show. In this example, it shows the Company field of the matched record in the Sponsors table. lookupOne and dot notation # Because lookupOne is creating a reference to a record, we can use dot notation to look up additional fields in that record. In the example above, Sponsors.lookupOne(Contact_Email=$Registration_Email) is checking if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. What if we also wanted to look up the sponsor level? We can add .Sponsor_Level to the lookupOne formula, and, if a match is found, look up the value in the sponsor level column for the matched record. The entire formula would be Sponsors.lookupOne(Contact_Email=$Registration_Email).Sponsor_Level . Now, we have the Sponsor Level listed in the All Registrations table for those attendees whose emails also appear on the sponsor list. lookupOne and sort_by # When the lookupOne function encounters multiple matching results, it returns the first one by row ID. The optional sort_by parameter can be used to sort these results by another field, to determine which one would be returned as the first match. You can also prefix the column ID with \u201c-\u201d to reverse the order. For instance, consider this example from the Class Enrollment template. This template tracks enrollment for extracurricular and other classes - logging information for students, families, and staff. On this page, we have a list of students and their respective information. Additionally, we have a Families page that outlines the parent of each student and we\u2019d like to find which student in each family is the oldest. So, we would create an oldest student column. Then, the following formula would look at the Students table, find the specific students associated with each family, sort them by their birthday, and return the one student with the earliest birthday: Students.lookupOne(Family=$id, sort_by=\"Birthday\") In this case, this would return: Raddon, Brockie. Alternatively, if we want to find the youngest student, the formula would include \u201c-\u201c: Students.lookupOne(Family=$id, sort_by=\"-Birthday\u201d) In this case, this would return: Raddon, Care. Understanding record sets # Sometimes a record may reference multiple records in another table. Multiple references can be made with a Reference List Column. A great example of this is seen on the Habit Tracker template. On the Habits + Goals page, we have a list of habits and a goal for how often we wish to complete that habit. On the Habit Tracker page, we have a Reference List column labeled Habits Completed that references the records on our Habits + Goals page. The only difference between a Reference column and a Reference List column is the ability to select multiple references. This creates a set of records which can be used in formulas. Reference lists and dot notation # Similar to references, you can use Dot Notation with reference lists. Building on our prior example of attendees at a conference, suppose we have a list of registrants for an event and want to find the balance for each registrant. To do this, we can use dot notation. Here, $Registrants is a reference list. Our Great Outdoors Expo has 4 registrants. We can see the list of registrants in the Registrants column. This list is a reference to the Name column of the All Registrants table. With a reference list, dot-notation returns a list of all the selected field; $Registrants.Balance is a list of the Balances for each attendee in the list of $Registrants . This follows the format $[A].[B] where [A] is the name of the Reference List column and [B] is the name of the column in the referenced table you wish to pull data from. We\u2019ll learn how to find the sum of these balances in Working with Record Sets . lookupRecords # You can also get a list of references using lookupRecords . The formula for lookupRecords follows this format: [Table_Name].lookupRecords([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Suppose we want a list of the events attended by each person in our Email List table. We can use lookupRecords to do this. First, we need to lookup records where the email listed in the All Registrations table matches an email in this list. Then, find the event associated with each of those records. Following the format above, our initial formula is: All_Registrations.lookupRecords(Registration_Email=$Email) All_Registrations.lookupRecords(Registration_Email=$Email) returns a list of record IDs for each record in the All Registrations table where the Registration Email matches the Email in this row of the Email List table. Next, we need to find the Event associated with each of these records. To do this, we can use dot notation. All_Registrations.lookupRecords(Registration_Email=$Email).Event will return the value from the Event column for each record found. We saw similar results using the lookupOne function. It\u2019s helpful to change the column type to Reference List, as you see in the screenshot below. Then, if there is a match, the reference list column will point to the entire record for each match. Like any reference list column, you can select which field you want to show for the matched records. In this example, it shows the Event field of the Events table for each matched record in the Attendees table. Reverse lookups # LookupRecords works a bit differently if a reference exists between two tables. With a reverse lookup, we can use the record ID to find a record. Every row has a numeric id (available as $id in formulas) that is unique within that table. You can reveal the ID by adding a formula column where formula is $id Let\u2019s take a look at the Registrants column of the Events table. The formula used here is All_Registrations.lookupRecords(Event=$id) . We use the id to find a match because in the All Registrations table, the Event column is a reference column which means its value is a record\u2019s id. Because All_Registrations.Event is a reference column pointing to an Event record in the Events table, we can match the id stored in the reference column to the ids of records in the Events table. That\u2019s why the argument in the formula is Event=$id . We use the existing reference, just in reverse - hence the name, Reverse Lookup. If you\u2019d like a video walkthrough of a reverse lookup, we have an example in our Build with Grist Webinar - Trigger Formulas v. Formulas . Working with record sets # lookupRecords can also be used within other formulas. SUM() can be useful to find a sum of all numbers in a list of records. Once you find your list of records using the lookupRecords function and dot notation, you can use SUM() to sum all values returned, like you see in this formula: SUM(Table.lookupRecords(Column_A=$Column_B).Column_C) You can also do this on a reference list because a reference list is the same thing, a list of records. SUM($RefList.Column) In the Reference lists and dot notation section, we used the Registrants column and dot notation to find the balance for each person in our list of Registrants. We can use SUM() with our prior formula to find the total balance. SUM($Registrants.Balance) We can also use lookupRecords to get the list of references, rather than using a reference list column, then find the sum of the balance for all registrants. This method is used in the Ticket Revenue column of the Events table using the following formula: SUM(All_Registrations.lookupRecords(Event=$id).Balance) All_Registrations.lookupRecords(Event=$id).Balance finds all records in the All Registrations table where the Event column matches the ID of the row in this table, Events. Using dot notation, we find the Balance for each of the records found. Then SUM() sums the balances of all records found. You can also iterate through a Reference List using a Python for loop. An example of this can be seen in the Balance (\u2018for\u2019 loop) column in the Events table. When iterating, each element is a Reference so dot-notation can be used here as well. To find the sum of the balance for all registrants, we use the following formula: SUM(person.Balance for person in $Registrants) This does the same thing as our lookupRecords formula we saw above. $Registrants is our reference list. For each record ( person ) in our list of Registrants, we find the Balance. Then, sum all balances together. In this formula, person is a variable that represents each element in our list and could be replaced with any other variable. If you\u2019d like to learn more about Data Structures and List Comprehension in Python 3, Python.org is a great resource. len() can be useful to get the number of items within a list. Once you find your list of records using the lookupRecords function, you can use len() to count the number of records returned, like you see in this formula: len(Table.lookupRecords(Column_A=$Column_B)) You can also do this on a reference list. len($RefList) We want to see how many events our Sponsors have attended. We can use lookupRecords to do this. The following formula is used in the Events Attended column of the Sponsors table. len(All_Registrations.lookupRecords(Sponsor=$id)) Let\u2019s break down the two parts of this formula, working from the inside out. All_Registrations.lookupRecords(Sponsor=$id) is looking for matches where the record in the Sponsor column of the All Registrations table has the same ID as the record in this row of the Sponsors table. All records in the All Registrations table that match are added to a list of records. Try writing the formula without len() to see what Grist returns. It should look something like this. That\u2019s a list of records. len() counts how many records are in that list. We can also include multiple arguments in a lookupRecords formula. An example of this can be found in the Count column of the Classes table of the Class Enrollment template. This column shows us how many students are enrolled in each class. The formula used here is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This lookup uses two fields. It will look for records in the Enrollment table where Status is \u201cConfirmed\u201d and the Class column matches the ID of the row in this table. Because the Class column is referencing the Classes table, we use the record ID $id in the lookup. Finally, len() counts the items in the list returned by Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\") . Average, min and max are a few of the other functions that can be used with lookupRecords formulas. See all available functions on our Function reference page.","title":"References and Lookups"},{"location":"references-lookups/#using-references-and-lookups-in-formulas","text":"Reference and Reference List columns in Grist allow one table to create an explicit reference to another. A common example of this is seen in the Class Enrollment template. On the Staff page, we have a list of staff members. On the classes page, we have a reference column labeled Instructor that references the records on our Staff page. Keep in mind, it\u2019s not just referencing the Full Name column but the entire record associated with the selected instructor.","title":"Using References and Lookups in Formulas"},{"location":"references-lookups/#reference-columns-and-dot-notation","text":"Using a Reference column within a formula can make it easy to get any data from the referenced record. To do this, we use dot notation. It uses the format $A.B where A is the name of the reference column and B is the name of the column in the referenced table that we want to pull data from. Let\u2019s see this in action on the Enrollment View page of the Class Enrollment template. Dot notation is used in the Class_Times column of the ENROLLMENTS table, found at the bottom right of the Enrollment View page. We can see that the Class_Times column is using a formula with dot notation. Using the format $A.B described above, we can figure out that Class is the name of the reference column and Times is the name of the column in the referenced table. Let\u2019s track this back to where it\u2019s pulling from - since the reference column is Class, we can look at that column\u2019s information to find out what table it is pulling from. The Class column references data from the Classes table. Therefore, the Class_Times column is pulling from the Times column of the Classes table.","title":"Reference columns and dot notation"},{"location":"references-lookups/#chaining","text":"If the reference lookup returns a reference, this can be chained. Perhaps we want to add the Instructor\u2019s phone number to the Enrollments table. We can use the Class reference column to pull the instructor\u2019s information from the Classes table. As you can see in the screenshot above, the instructor column is a reference column itself. If we follow the format from before, our dot notation would be $Class.Instructor but the Instructor column points to the entire record of the instructor so we need to tell it what information we want from this record, creating a chain. The instructor column references the Staff table so we navigate there to find out what column we need to pull information from in order to get the phone number. The column that contains the instructor\u2019s phone number is Phone. Putting this all together, our dot notation for the instructor\u2019s phone number would be $Class.Instructor.Phone What happens if we leave our formulas as $Class.Instructor ? You will see a numeric record ID of the record in the Staff table that the Instructor column points to. That\u2019s what a reference column really stores. If you change the type of this formula column to Reference, you will be able to select a column to show, such as the Full Name. Another way to see the name is to chain the dot-notation, as we did for phone: $Class.Instructor.Full_Name .","title":"Chaining"},{"location":"references-lookups/#lookupone","text":"Another way to point to a record is using Table.lookupOne(...) function. lookupOne allows you to look up a record by some fields, similar to Excel\u2019s VLOOKUP. In fact, Grist\u2019s version of VLOOKUP is merely an alias for lookupOne. lookupOne is rarely useful in Grist, because using a Reference type column is usually the preferred solution to connect records. However, on some occasions, lookupOne can be useful. One situation is when you have two sets of data which overlap even though they represent something different and perhaps come from different sources. An example of this can be found in our Event Sponsors + Attendees (References and Lookups) document which is a modified version of the Event Sponsors + Attendees template, available in our template gallery . Let\u2019s say that you run an event and have a list of registered attendees, as well as Sponsors. Registered attendees are stored in the All Registrations table, perhaps populated via a form integration. Sponsors are listed in a separate table, with fields related to their sponsorship, and perhaps maintained by another team. Both tables contain email addresses which identify attendees and sponsors. Sometimes a sponsor may register to attend the event. In that case, you\u2019ll have an Attendee record with an email address that also appears in the Sponsors table. That\u2019s useful to know for someone looking at the attendee list. You can lookup a record in the sponsors table by email address by using a lookupOne formula. The Sponsor column in the All Registrations table does just that using this formula: Sponsors.lookupOne(Contact_Email=$Registration_Email) This formula is looking to see if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. The general format for a lookupOne formula is: [Table_Name].lookupOne([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Such a formula returns a reference. In the screenshot above, you can see the lookup result returns Sponsors[#] . The number it returns between square brackets is the record ID of the lookup result. Where it returns Sponsors[0] , no match was found. It\u2019s often a good idea to create a column for the lookup result and change its type to Reference, as you see in the screenshot below. Then, if there is a match, the reference column will point to the entire matched record. Like any reference column, you can select which field from that record to show. In this example, it shows the Company field of the matched record in the Sponsors table.","title":"lookupOne"},{"location":"references-lookups/#lookupone-and-dot-notation","text":"Because lookupOne is creating a reference to a record, we can use dot notation to look up additional fields in that record. In the example above, Sponsors.lookupOne(Contact_Email=$Registration_Email) is checking if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. What if we also wanted to look up the sponsor level? We can add .Sponsor_Level to the lookupOne formula, and, if a match is found, look up the value in the sponsor level column for the matched record. The entire formula would be Sponsors.lookupOne(Contact_Email=$Registration_Email).Sponsor_Level . Now, we have the Sponsor Level listed in the All Registrations table for those attendees whose emails also appear on the sponsor list.","title":"lookupOne and dot notation"},{"location":"references-lookups/#lookupone-and-sort_by","text":"When the lookupOne function encounters multiple matching results, it returns the first one by row ID. The optional sort_by parameter can be used to sort these results by another field, to determine which one would be returned as the first match. You can also prefix the column ID with \u201c-\u201d to reverse the order. For instance, consider this example from the Class Enrollment template. This template tracks enrollment for extracurricular and other classes - logging information for students, families, and staff. On this page, we have a list of students and their respective information. Additionally, we have a Families page that outlines the parent of each student and we\u2019d like to find which student in each family is the oldest. So, we would create an oldest student column. Then, the following formula would look at the Students table, find the specific students associated with each family, sort them by their birthday, and return the one student with the earliest birthday: Students.lookupOne(Family=$id, sort_by=\"Birthday\") In this case, this would return: Raddon, Brockie. Alternatively, if we want to find the youngest student, the formula would include \u201c-\u201c: Students.lookupOne(Family=$id, sort_by=\"-Birthday\u201d) In this case, this would return: Raddon, Care.","title":"lookupOne and sort_by"},{"location":"references-lookups/#understanding-record-sets","text":"Sometimes a record may reference multiple records in another table. Multiple references can be made with a Reference List Column. A great example of this is seen on the Habit Tracker template. On the Habits + Goals page, we have a list of habits and a goal for how often we wish to complete that habit. On the Habit Tracker page, we have a Reference List column labeled Habits Completed that references the records on our Habits + Goals page. The only difference between a Reference column and a Reference List column is the ability to select multiple references. This creates a set of records which can be used in formulas.","title":"Understanding record sets"},{"location":"references-lookups/#reference-lists-and-dot-notation","text":"Similar to references, you can use Dot Notation with reference lists. Building on our prior example of attendees at a conference, suppose we have a list of registrants for an event and want to find the balance for each registrant. To do this, we can use dot notation. Here, $Registrants is a reference list. Our Great Outdoors Expo has 4 registrants. We can see the list of registrants in the Registrants column. This list is a reference to the Name column of the All Registrants table. With a reference list, dot-notation returns a list of all the selected field; $Registrants.Balance is a list of the Balances for each attendee in the list of $Registrants . This follows the format $[A].[B] where [A] is the name of the Reference List column and [B] is the name of the column in the referenced table you wish to pull data from. We\u2019ll learn how to find the sum of these balances in Working with Record Sets .","title":"Reference lists and dot notation"},{"location":"references-lookups/#lookuprecords","text":"You can also get a list of references using lookupRecords . The formula for lookupRecords follows this format: [Table_Name].lookupRecords([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Suppose we want a list of the events attended by each person in our Email List table. We can use lookupRecords to do this. First, we need to lookup records where the email listed in the All Registrations table matches an email in this list. Then, find the event associated with each of those records. Following the format above, our initial formula is: All_Registrations.lookupRecords(Registration_Email=$Email) All_Registrations.lookupRecords(Registration_Email=$Email) returns a list of record IDs for each record in the All Registrations table where the Registration Email matches the Email in this row of the Email List table. Next, we need to find the Event associated with each of these records. To do this, we can use dot notation. All_Registrations.lookupRecords(Registration_Email=$Email).Event will return the value from the Event column for each record found. We saw similar results using the lookupOne function. It\u2019s helpful to change the column type to Reference List, as you see in the screenshot below. Then, if there is a match, the reference list column will point to the entire record for each match. Like any reference list column, you can select which field you want to show for the matched records. In this example, it shows the Event field of the Events table for each matched record in the Attendees table.","title":"lookupRecords"},{"location":"references-lookups/#reverse-lookups","text":"LookupRecords works a bit differently if a reference exists between two tables. With a reverse lookup, we can use the record ID to find a record. Every row has a numeric id (available as $id in formulas) that is unique within that table. You can reveal the ID by adding a formula column where formula is $id Let\u2019s take a look at the Registrants column of the Events table. The formula used here is All_Registrations.lookupRecords(Event=$id) . We use the id to find a match because in the All Registrations table, the Event column is a reference column which means its value is a record\u2019s id. Because All_Registrations.Event is a reference column pointing to an Event record in the Events table, we can match the id stored in the reference column to the ids of records in the Events table. That\u2019s why the argument in the formula is Event=$id . We use the existing reference, just in reverse - hence the name, Reverse Lookup. If you\u2019d like a video walkthrough of a reverse lookup, we have an example in our Build with Grist Webinar - Trigger Formulas v. Formulas .","title":"Reverse lookups"},{"location":"references-lookups/#working-with-record-sets","text":"lookupRecords can also be used within other formulas. SUM() can be useful to find a sum of all numbers in a list of records. Once you find your list of records using the lookupRecords function and dot notation, you can use SUM() to sum all values returned, like you see in this formula: SUM(Table.lookupRecords(Column_A=$Column_B).Column_C) You can also do this on a reference list because a reference list is the same thing, a list of records. SUM($RefList.Column) In the Reference lists and dot notation section, we used the Registrants column and dot notation to find the balance for each person in our list of Registrants. We can use SUM() with our prior formula to find the total balance. SUM($Registrants.Balance) We can also use lookupRecords to get the list of references, rather than using a reference list column, then find the sum of the balance for all registrants. This method is used in the Ticket Revenue column of the Events table using the following formula: SUM(All_Registrations.lookupRecords(Event=$id).Balance) All_Registrations.lookupRecords(Event=$id).Balance finds all records in the All Registrations table where the Event column matches the ID of the row in this table, Events. Using dot notation, we find the Balance for each of the records found. Then SUM() sums the balances of all records found. You can also iterate through a Reference List using a Python for loop. An example of this can be seen in the Balance (\u2018for\u2019 loop) column in the Events table. When iterating, each element is a Reference so dot-notation can be used here as well. To find the sum of the balance for all registrants, we use the following formula: SUM(person.Balance for person in $Registrants) This does the same thing as our lookupRecords formula we saw above. $Registrants is our reference list. For each record ( person ) in our list of Registrants, we find the Balance. Then, sum all balances together. In this formula, person is a variable that represents each element in our list and could be replaced with any other variable. If you\u2019d like to learn more about Data Structures and List Comprehension in Python 3, Python.org is a great resource. len() can be useful to get the number of items within a list. Once you find your list of records using the lookupRecords function, you can use len() to count the number of records returned, like you see in this formula: len(Table.lookupRecords(Column_A=$Column_B)) You can also do this on a reference list. len($RefList) We want to see how many events our Sponsors have attended. We can use lookupRecords to do this. The following formula is used in the Events Attended column of the Sponsors table. len(All_Registrations.lookupRecords(Sponsor=$id)) Let\u2019s break down the two parts of this formula, working from the inside out. All_Registrations.lookupRecords(Sponsor=$id) is looking for matches where the record in the Sponsor column of the All Registrations table has the same ID as the record in this row of the Sponsors table. All records in the All Registrations table that match are added to a list of records. Try writing the formula without len() to see what Grist returns. It should look something like this. That\u2019s a list of records. len() counts how many records are in that list. We can also include multiple arguments in a lookupRecords formula. An example of this can be found in the Count column of the Classes table of the Class Enrollment template. This column shows us how many students are enrolled in each class. The formula used here is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This lookup uses two fields. It will look for records in the Enrollment table where Status is \u201cConfirmed\u201d and the Class column matches the ID of the row in this table. Because the Class column is referencing the Classes table, we use the record ID $id in the lookup. Finally, len() counts the items in the list returned by Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\") . Average, min and max are a few of the other functions that can be used with lookupRecords formulas. See all available functions on our Function reference page.","title":"Working with record sets"},{"location":"register-as-consultant/","text":"Register to be a Grist consultant # .creg-form { line-height: initial; } .form-group .control-label { font-weight: normal; } .creg-button { margin: 24px 0; background-color: #11b683; border: none; color: white; } .creg-button:hover { background-color: #009058; border: none; color: white; } #creg-submitted, #creg-error { display: none; margin-top: 40px; } Are you a freelancer? Do you enjoy building in Grist and organizing data? We\u2019d be glad to support you, and to add you to our list of independent Grist consultants. Please fill out the information below, and we\u2019ll get in touch with you. Your Name: Email: Phone: Company: Website: Your technical background: Excel: None Beginner Intermediate Advanced SQL: None Beginner Intermediate Advanced Python: None Beginner Intermediate Advanced Javascript: None Beginner Intermediate Advanced Grist: None Beginner Intermediate Advanced Are you interested in receiving Grist training? Interested Submit Thank you for registering! We'll be in touch. Meanwhile, you are welcome to reach out to us with any questions, at support@getgrist.com . Could not submit the form. Try again function formDataToObj(formElem) { const formData = new FormData(formElem); const data = {}; for (const pair of formData.entries()) { if (typeof pair[1] === 'string') { data[pair[0]] = [pair[1]]; } } return data; } const form = document.getElementById('creg-form'); form.addEventListener('submit', (ev) => { ev.preventDefault(); const data = formDataToObj(form); const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } }; window.fetch(form.action, options) .then(resp => { return resp.json() .then(body => { console.log(\"BODY\", body); if (resp.status !== 200) { throw new Error(\"Could not submit the form: \" + (body && body.error || \"unknown error\")); } console.log(\"Form submitted\", body); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-submitted').style.display = 'block'; }) }) .catch(err => { console.warn(\"ERROR\", err); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-error-message').textContent = String(err); document.getElementById('creg-error').style.display = 'block'; }); });","title":"Register to be a Grist consultant"},{"location":"register-as-consultant/#register-to-be-a-grist-consultant","text":".creg-form { line-height: initial; } .form-group .control-label { font-weight: normal; } .creg-button { margin: 24px 0; background-color: #11b683; border: none; color: white; } .creg-button:hover { background-color: #009058; border: none; color: white; } #creg-submitted, #creg-error { display: none; margin-top: 40px; } Are you a freelancer? Do you enjoy building in Grist and organizing data? We\u2019d be glad to support you, and to add you to our list of independent Grist consultants. Please fill out the information below, and we\u2019ll get in touch with you. Your Name: Email: Phone: Company: Website: Your technical background: Excel: None Beginner Intermediate Advanced SQL: None Beginner Intermediate Advanced Python: None Beginner Intermediate Advanced Javascript: None Beginner Intermediate Advanced Grist: None Beginner Intermediate Advanced Are you interested in receiving Grist training? Interested Submit Thank you for registering! We'll be in touch. Meanwhile, you are welcome to reach out to us with any questions, at support@getgrist.com . Could not submit the form. Try again function formDataToObj(formElem) { const formData = new FormData(formElem); const data = {}; for (const pair of formData.entries()) { if (typeof pair[1] === 'string') { data[pair[0]] = [pair[1]]; } } return data; } const form = document.getElementById('creg-form'); form.addEventListener('submit', (ev) => { ev.preventDefault(); const data = formDataToObj(form); const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } }; window.fetch(form.action, options) .then(resp => { return resp.json() .then(body => { console.log(\"BODY\", body); if (resp.status !== 200) { throw new Error(\"Could not submit the form: \" + (body && body.error || \"unknown error\")); } console.log(\"Form submitted\", body); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-submitted').style.display = 'block'; }) }) .catch(err => { console.warn(\"ERROR\", err); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-error-message').textContent = String(err); document.getElementById('creg-error').style.display = 'block'; }); });","title":"Register to be a Grist consultant"},{"location":"rest-api/","text":"Grist API Usage # Grist has an API for manipulating documents, workspaces, and team sites. API Reference shows documentation of all available endpoints. Interactive API Console allows you to make API calls using your Grist login. Authentication # To access the Grist API, you\u2019ll need an API key. An API key is owned by a single user, and has the same permissions as that user. To enable API access for yourself, visit your Profile Settings . You can always find this page by clicking your profile picture or initial on the top right of the screen to open the account menu. Then select the \u201cProfile Settings\u201d option: This shows a dialog with all of your profile setting options. Scroll down to the \u201cAPI\u201d section. Click on the \u201cCreate\u201d button to create an API Key. You can now copy this key for use when making API calls. To be clear, copy the key in your profile settings, not the key in the above screenshot, which isn\u2019t a real one. You can revoke your API key by clicking \u201cRemove\u201d from Profile Settings at any time. You\u2019ll then have the option to create a new one if you wish. To test your api key, try this from the command-line (substituting your api key): curl -H \"Authorization: Bearer Waiting for data...The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support. Adding a custom widget # To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html Access level # When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget. Invoice example # The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord . Creating a custom widget # As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API. Column mapping # Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping. Widget options # If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen. Custom Widget linking # Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'}); Premade Custom Widgets # All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown. Advanced Charts # The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration. Copy to clipboard # Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Dropbox Embedder # View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template. Grist Video Player # Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget. HTML Viewer # The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Image Viewer # View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar ! JupyterLite Notebook # This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
Waiting for data...The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support.","title":"Minimal example"},{"location":"widget-custom/#adding-a-custom-widget","text":"To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html","title":"Adding a custom widget"},{"location":"widget-custom/#access-level","text":"When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget.","title":"Access level"},{"location":"widget-custom/#invoice-example","text":"The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord .","title":"Invoice example"},{"location":"widget-custom/#creating-a-custom-widget","text":"As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API.","title":"Creating a custom widget"},{"location":"widget-custom/#column-mapping","text":"Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping.","title":"Column mapping"},{"location":"widget-custom/#widget-options","text":"If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen.","title":"Widget options"},{"location":"widget-custom/#custom-widget-linking","text":"Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'});","title":"Custom Widget linking"},{"location":"widget-custom/#premade-custom-widgets","text":"All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown.","title":"Premade Custom Widgets"},{"location":"widget-custom/#advanced-charts","text":"The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration.","title":"Advanced Charts"},{"location":"widget-custom/#copy-to-clipboard","text":"Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"Copy to clipboard"},{"location":"widget-custom/#dropbox-embedder","text":"View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template.","title":"Dropbox Embedder"},{"location":"widget-custom/#grist-video-player","text":"Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget.","title":"Grist Video Player"},{"location":"widget-custom/#html-viewer","text":"The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"HTML Viewer"},{"location":"widget-custom/#image-viewer","text":"View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar !","title":"Image Viewer"},{"location":"widget-custom/#jupyterlite-notebook","text":"This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
from other elements using blank lines. Configuring submission options # You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel. Publishing your form # Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error. Form submissions # After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form"},{"location":"widget-form/#page-widget-form","text":"The form widget allows you to collect data in a form view which populates your Grist data table upon submission.","title":"Page widget: Form"},{"location":"widget-form/#setting-up-your-data","text":"Create a table containing the columns of data you wish to populate via form.","title":"Setting up your data"},{"location":"widget-form/#creating-your-form","text":"Add a form widget from the \u201cAdd New\u201d menu. Select the data table you wish to populate with form data. Then, customize the form to your heart\u2019s desire! By default, the form view will include elements for headers and descriptions as well as all columns (fields) from the underlying data table.","title":"Creating your form"},{"location":"widget-form/#adding-and-removing-elements","text":"To add additional form elements, click the + icon at the bottom of the form. From the menu, you can add the following elements: New Question: Select a column type to create a new field. \u201c\u2022\u2022\u2022 More >\u201d will open an expanded menu listing all column types. Adding a new question will add a new column to the underlying data table. Unmapped Fields: Lists any hidden fields from the underlying data table. Building Blocks: Customize further by adding these additional elements! You can remove any element from the form by hovering over the object and clicking the trash icon to delete. You can hide any uneccessary fields from the form by hovering over the object and clicking the x icon.","title":"Adding and removing elements"},{"location":"widget-form/#configuring-fields","text":"You can provide alternative titles for your form fields, rather than use the same column name from the underlying data table. For example, on our form, we have a toggle that is titled \u201cMay we contact you?\u201d. In the data table, this column is labeled \u201cOk to Contact?\u201d. Field titles can be configured under the \u201cField\u201d tab of the creator panel. To make a form field required, check the box next to \u201cRequired field\u201d. If a user attempts to submit a form without filling in the required field, they will get an alert to fill out the field.","title":"Configuring fields"},{"location":"widget-form/#configuring-building-blocks","text":"Header and Paragraph building blocks can be edited either directly in the block or from the creator panel. In the creator panel, you have text alignment options available. For additional formatting, both elements allow the use of Markdown formatting. For help on Markdown formatting, check out the Markdown Guide . HTML Formatting HTML tags can be used in Markdown-formatted text. Be sure to separate block-level HTML elements like
from other elements using blank lines.","title":"Configuring building blocks"},{"location":"widget-form/#configuring-submission-options","text":"You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel.","title":"Configuring submission options"},{"location":"widget-form/#publishing-your-form","text":"Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error.","title":"Publishing your form"},{"location":"widget-form/#form-submissions","text":"After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form submissions"},{"location":"widget-table/","text":"Page widget: Table # The Table widget is a versatile spreadsheet-like grid. Here are some useful features to know. Column operations # Resize columns : Click on the line between column headers, and drag it to resize columns. Reorder columns : With a column selected, drag its header to move it to a different place relative to other columns. (You can also do this by reordering fields in the widget options panel.) Rename columns : With a column selected, click its header to rename it. Hit Enter to save the new name. Add columns : Click the \u201c+\u201d icon on the right of all the column headers to add a new column, or show any of the hidden columns. The column menu also allows inserting a new column next to an existing column, as do the keyboard shortcuts Alt + + (insert before) and Alt + = (insert after). After adding a column, the column name (set by default to \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc.) is immediately selected and highlighted to let you quickly rename it. Just type in the new name and hit Enter , or hit Escape to keep the default name. Hide columns : Move the mouse over the column header and click the triangle to open the column menu. Click \u201cHide column\u201d to hide the column. The column remains in the underlying data, and can be shown again using the \u201c+\u201d icon on the right of the column headers, or the field list in the widget options panel. Delete columns : Delete the actual column of data using the column menu option, or the Alt + Minus keyboard shortcut. The table is the only widget that allows deleting a column. Note: deleting and hiding are different. Hiding a columns removes it only from the current page widget, but leaves it in the data and available to formulas. Deleting a column removes it from everywhere. (Of course, undo still works for either operation!) Row operations # Add rows : Type into the last row in a table, which is highlighted to indicate that it\u2019s a placeholder for adding new records. Right click a row number to insert a blank row next to an existing row, or use the keyboard shortcuts \u2318 \u21e7 = (Mac) or Ctrl + Shift + = (Windows) to insert before, and \u2318 = (Mac) or Ctrl + = (Windows) to insert after. Delete rows : Right click a row number and select the \u201cDelete\u201d option to delete a row, or use the \u2318 + Minus (Mac) or Ctrl + Minus (Windows) shortcut. If you select a range of cells first, either of these delete actions will delete all rows included in the range. Link to rows : Right click a row number and select \u201cCopy anchor link\u201d to copy a link to the selected cell of that row. The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document. Navigation and selection # Navigate using shortcuts : Use keyboard shortcuts to navigate the grid: Tab , Shift + Tab Move to the next or previous column, saving changes if editing a cell. \u2318 + Up (Mac) or Ctrl + Up (Windows) Move up to the first row. \u2318 + Down (Mac) or Ctrl + Down (Windows) Move down to the last row. Home or Fn + \u2190 (Mac) Move to the beginning of a row. End or Fn + \u2192 (Mac) Move to the end of a row. PageDown or Fn + \u2193 (Mac) Move down one page of rows. PageUp or Fn + \u2191 (Mac) Move up one page of rows. Alt + Down , Alt + Up Move down or up five rows. Select ranges : Click and drag a mouse across the grid to select a range of cells to copy (copy and paste using the usual keyboard shortcuts for your computer). Another way to select a range is to click one cell, and then hold Shift while clicking another cell, or while navigating with the arrow keys. Fill down data : Select a range of cells, and hit \u2318 + D (Mac) or Ctrl + D (Windows) to fill the whole selected range with the values of the cells in the top row of the range. Customization # Customize table looks : In the widget options panel, you can turn off horizontal or vertical grid lines, or turn on zebra striping. For example, this lets you change the look of your grid to a list like this:","title":"Table widget"},{"location":"widget-table/#page-widget-table","text":"The Table widget is a versatile spreadsheet-like grid. Here are some useful features to know.","title":"Page widget: Table"},{"location":"widget-table/#column-operations","text":"Resize columns : Click on the line between column headers, and drag it to resize columns. Reorder columns : With a column selected, drag its header to move it to a different place relative to other columns. (You can also do this by reordering fields in the widget options panel.) Rename columns : With a column selected, click its header to rename it. Hit Enter to save the new name. Add columns : Click the \u201c+\u201d icon on the right of all the column headers to add a new column, or show any of the hidden columns. The column menu also allows inserting a new column next to an existing column, as do the keyboard shortcuts Alt + + (insert before) and Alt + = (insert after). After adding a column, the column name (set by default to \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc.) is immediately selected and highlighted to let you quickly rename it. Just type in the new name and hit Enter , or hit Escape to keep the default name. Hide columns : Move the mouse over the column header and click the triangle to open the column menu. Click \u201cHide column\u201d to hide the column. The column remains in the underlying data, and can be shown again using the \u201c+\u201d icon on the right of the column headers, or the field list in the widget options panel. Delete columns : Delete the actual column of data using the column menu option, or the Alt + Minus keyboard shortcut. The table is the only widget that allows deleting a column. Note: deleting and hiding are different. Hiding a columns removes it only from the current page widget, but leaves it in the data and available to formulas. Deleting a column removes it from everywhere. (Of course, undo still works for either operation!)","title":"Column operations"},{"location":"widget-table/#row-operations","text":"Add rows : Type into the last row in a table, which is highlighted to indicate that it\u2019s a placeholder for adding new records. Right click a row number to insert a blank row next to an existing row, or use the keyboard shortcuts \u2318 \u21e7 = (Mac) or Ctrl + Shift + = (Windows) to insert before, and \u2318 = (Mac) or Ctrl + = (Windows) to insert after. Delete rows : Right click a row number and select the \u201cDelete\u201d option to delete a row, or use the \u2318 + Minus (Mac) or Ctrl + Minus (Windows) shortcut. If you select a range of cells first, either of these delete actions will delete all rows included in the range. Link to rows : Right click a row number and select \u201cCopy anchor link\u201d to copy a link to the selected cell of that row. The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document.","title":"Row operations"},{"location":"widget-table/#navigation-and-selection","text":"Navigate using shortcuts : Use keyboard shortcuts to navigate the grid: Tab , Shift + Tab Move to the next or previous column, saving changes if editing a cell. \u2318 + Up (Mac) or Ctrl + Up (Windows) Move up to the first row. \u2318 + Down (Mac) or Ctrl + Down (Windows) Move down to the last row. Home or Fn + \u2190 (Mac) Move to the beginning of a row. End or Fn + \u2192 (Mac) Move to the end of a row. PageDown or Fn + \u2193 (Mac) Move down one page of rows. PageUp or Fn + \u2191 (Mac) Move up one page of rows. Alt + Down , Alt + Up Move down or up five rows. Select ranges : Click and drag a mouse across the grid to select a range of cells to copy (copy and paste using the usual keyboard shortcuts for your computer). Another way to select a range is to click one cell, and then hold Shift while clicking another cell, or while navigating with the arrow keys. Fill down data : Select a range of cells, and hit \u2318 + D (Mac) or Ctrl + D (Windows) to fill the whole selected range with the values of the cells in the top row of the range.","title":"Navigation and selection"},{"location":"widget-table/#customization","text":"Customize table looks : In the widget options panel, you can turn off horizontal or vertical grid lines, or turn on zebra striping. For example, this lets you change the look of your grid to a list like this:","title":"Customization"},{"location":"workspaces/","text":"Workspaces # Documents can be collected in folders called \u201cworkspaces\u201d which can be shared as a single unit with other users. This is convenient, for example, for documents related to a single project. When a site is first created, it has a single workspace called \u201cHome.\u201d You can rename that workspace if you like, by hovering over the workspace name on the left bar, clicking on the three-dots icon, and selecting \u201cRename\u201d. Even better, you can create new workspaces, so you can group your documents and share them as a unit. To add a document to a specific workspace, click on that workspace in the left bar, then select \u201cAdd New\u201d, then \u201cCreate empty document\u201d (or \u201cImport document\u201d). To move a document from one workspace to another, hover over the document, click on the three-dots icon to the right of the document\u2019s name, and select \u201cMove\u201d. You\u2019ll then have the option to pick the workspace you want. Managing access # On team sites , workspace owners can control who has access to a workspace using \u201cManage Users.\u201d The controls are just like for sharing documents . Workspaces in personal sites cannot be shared. Viewers of a team site will also be viewers of all its workspaces. Likewise for editors and owners. To exclude a workspace from inheriting team site viewers, editors, and owners, set \u201cInherit Access\u201d to \u201cNone\u201d. You can also set it to \u201cView Only\u201d to limit inheritance to view rights, or \u201cView & Edit\u201d to limit inheritance to view and edit rights (excluding the right to control sharing options). It is possible to be an editor/owner of a workspace and not be able to open all documents within that workspace. This can happen if an owner of a document limits inheritance to \u201cNone.\u201d You will still see the document listed, so that if you ever decide to delete the workspace you\u2019ll know what you\u2019re deleting. Viewers of a workspace will only see documents they have access to listed.","title":"Workspaces"},{"location":"workspaces/#workspaces","text":"Documents can be collected in folders called \u201cworkspaces\u201d which can be shared as a single unit with other users. This is convenient, for example, for documents related to a single project. When a site is first created, it has a single workspace called \u201cHome.\u201d You can rename that workspace if you like, by hovering over the workspace name on the left bar, clicking on the three-dots icon, and selecting \u201cRename\u201d. Even better, you can create new workspaces, so you can group your documents and share them as a unit. To add a document to a specific workspace, click on that workspace in the left bar, then select \u201cAdd New\u201d, then \u201cCreate empty document\u201d (or \u201cImport document\u201d). To move a document from one workspace to another, hover over the document, click on the three-dots icon to the right of the document\u2019s name, and select \u201cMove\u201d. You\u2019ll then have the option to pick the workspace you want.","title":"Workspaces"},{"location":"workspaces/#managing-access","text":"On team sites , workspace owners can control who has access to a workspace using \u201cManage Users.\u201d The controls are just like for sharing documents . Workspaces in personal sites cannot be shared. Viewers of a team site will also be viewers of all its workspaces. Likewise for editors and owners. To exclude a workspace from inheriting team site viewers, editors, and owners, set \u201cInherit Access\u201d to \u201cNone\u201d. You can also set it to \u201cView Only\u201d to limit inheritance to view rights, or \u201cView & Edit\u201d to limit inheritance to view and edit rights (excluding the right to control sharing options). It is possible to be an editor/owner of a workspace and not be able to open all documents within that workspace. This can happen if an owner of a document limits inheritance to \u201cNone.\u201d You will still see the document listed, so that if you ever decide to delete the workspace you\u2019ll know what you\u2019re deleting. Viewers of a workspace will only see documents they have access to listed.","title":"Managing access"},{"location":"code/","text":"Plugin API # The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Intro to Plugin API"},{"location":"code/#plugin-api","text":"The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Plugin API"},{"location":"code/enums/GristData.GristObjCode/","text":"Enumeration: GristObjCode # GristData .GristObjCode Letter codes for CellValue types encoded as [code, args\u2026] tuples. Table of contents # Enumeration Members # Censored Date DateTime Dict Exception List LookUp Pending Reference ReferenceList Skip Unmarshallable Versions Enumeration Members # Censored # \u2022 Censored = \"C\" Date # \u2022 Date = \"d\" DateTime # \u2022 DateTime = \"D\" Dict # \u2022 Dict = \"O\" Exception # \u2022 Exception = \"E\" List # \u2022 List = \"L\" LookUp # \u2022 LookUp = \"l\" Pending # \u2022 Pending = \"P\" Reference # \u2022 Reference = \"R\" ReferenceList # \u2022 ReferenceList = \"r\" Skip # \u2022 Skip = \"S\" Unmarshallable # \u2022 Unmarshallable = \"U\" Versions # \u2022 Versions = \"V\"","title":"Enumeration: GristObjCode"},{"location":"code/enums/GristData.GristObjCode/#enumeration-gristobjcode","text":"GristData .GristObjCode Letter codes for CellValue types encoded as [code, args\u2026] tuples.","title":"Enumeration: GristObjCode"},{"location":"code/enums/GristData.GristObjCode/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/enums/GristData.GristObjCode/#enumeration-members","text":"Censored Date DateTime Dict Exception List LookUp Pending Reference ReferenceList Skip Unmarshallable Versions","title":"Enumeration Members"},{"location":"code/enums/GristData.GristObjCode/#enumeration-members_1","text":"","title":"Enumeration Members"},{"location":"code/enums/GristData.GristObjCode/#censored","text":"\u2022 Censored = \"C\"","title":"Censored"},{"location":"code/enums/GristData.GristObjCode/#date","text":"\u2022 Date = \"d\"","title":"Date"},{"location":"code/enums/GristData.GristObjCode/#datetime","text":"\u2022 DateTime = \"D\"","title":"DateTime"},{"location":"code/enums/GristData.GristObjCode/#dict","text":"\u2022 Dict = \"O\"","title":"Dict"},{"location":"code/enums/GristData.GristObjCode/#exception","text":"\u2022 Exception = \"E\"","title":"Exception"},{"location":"code/enums/GristData.GristObjCode/#list","text":"\u2022 List = \"L\"","title":"List"},{"location":"code/enums/GristData.GristObjCode/#lookup","text":"\u2022 LookUp = \"l\"","title":"LookUp"},{"location":"code/enums/GristData.GristObjCode/#pending","text":"\u2022 Pending = \"P\"","title":"Pending"},{"location":"code/enums/GristData.GristObjCode/#reference","text":"\u2022 Reference = \"R\"","title":"Reference"},{"location":"code/enums/GristData.GristObjCode/#referencelist","text":"\u2022 ReferenceList = \"r\"","title":"ReferenceList"},{"location":"code/enums/GristData.GristObjCode/#skip","text":"\u2022 Skip = \"S\"","title":"Skip"},{"location":"code/enums/GristData.GristObjCode/#unmarshallable","text":"\u2022 Unmarshallable = \"U\"","title":"Unmarshallable"},{"location":"code/enums/GristData.GristObjCode/#versions","text":"\u2022 Versions = \"V\"","title":"Versions"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/","text":"Interface: AddOrUpdateRecord # DocApiTypes .AddOrUpdateRecord JSON schema for api /record endpoint. Used in PUT method for adding or updating records. Table of contents # Properties # fields require Properties # fields # \u2022 Optional fields : Object The values we will place in particular columns, either overwriting values in an existing record, or setting initial values in a new record. Index signature # \u25aa [coldId: string ]: CellValue require # \u2022 require : { [coldId: string] : CellValue ; } & { id? : number } The values we expect to have in particular columns, either by matching with an existing record, or creating a new record.","title":"Interface: AddOrUpdateRecord"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#interface-addorupdaterecord","text":"DocApiTypes .AddOrUpdateRecord JSON schema for api /record endpoint. Used in PUT method for adding or updating records.","title":"Interface: AddOrUpdateRecord"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#properties","text":"fields require","title":"Properties"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#fields","text":"\u2022 Optional fields : Object The values we will place in particular columns, either overwriting values in an existing record, or setting initial values in a new record.","title":"fields"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#index-signature","text":"\u25aa [coldId: string ]: CellValue","title":"Index signature"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#require","text":"\u2022 require : { [coldId: string] : CellValue ; } & { id? : number } The values we expect to have in particular columns, either by matching with an existing record, or creating a new record.","title":"require"},{"location":"code/interfaces/DocApiTypes.MinimalRecord/","text":"Interface: MinimalRecord # DocApiTypes .MinimalRecord The row id of a record, without any of its content.","title":"Interface: MinimalRecord"},{"location":"code/interfaces/DocApiTypes.MinimalRecord/#interface-minimalrecord","text":"DocApiTypes .MinimalRecord The row id of a record, without any of its content.","title":"Interface: MinimalRecord"},{"location":"code/interfaces/DocApiTypes.NewRecord/","text":"Interface: NewRecord # DocApiTypes .NewRecord JSON schema for api /record endpoint. Used in POST method for adding new records. Table of contents # Properties # fields Properties # fields # \u2022 Optional fields : Object Initial values of cells in record. Optional, if not set cells are left blank. Index signature # \u25aa [coldId: string ]: CellValue","title":"Interface: NewRecord"},{"location":"code/interfaces/DocApiTypes.NewRecord/#interface-newrecord","text":"DocApiTypes .NewRecord JSON schema for api /record endpoint. Used in POST method for adding new records.","title":"Interface: NewRecord"},{"location":"code/interfaces/DocApiTypes.NewRecord/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/DocApiTypes.NewRecord/#properties","text":"fields","title":"Properties"},{"location":"code/interfaces/DocApiTypes.NewRecord/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/DocApiTypes.NewRecord/#fields","text":"\u2022 Optional fields : Object Initial values of cells in record. Optional, if not set cells are left blank.","title":"fields"},{"location":"code/interfaces/DocApiTypes.NewRecord/#index-signature","text":"\u25aa [coldId: string ]: CellValue","title":"Index signature"},{"location":"code/interfaces/DocApiTypes.Record/","text":"Interface: Record # DocApiTypes .Record JSON schema for api /record endpoint. Used in PATCH method for updating existing records.","title":"Interface: Record"},{"location":"code/interfaces/DocApiTypes.Record/#interface-record","text":"DocApiTypes .Record JSON schema for api /record endpoint. Used in PATCH method for updating existing records.","title":"Interface: Record"},{"location":"code/interfaces/DocApiTypes.RecordsPatch/","text":"Interface: RecordsPatch # DocApiTypes .RecordsPatch JSON schema for the body of api /record PATCH endpoint","title":"Interface: RecordsPatch"},{"location":"code/interfaces/DocApiTypes.RecordsPatch/#interface-recordspatch","text":"DocApiTypes .RecordsPatch JSON schema for the body of api /record PATCH endpoint","title":"Interface: RecordsPatch"},{"location":"code/interfaces/DocApiTypes.RecordsPost/","text":"Interface: RecordsPost # DocApiTypes .RecordsPost JSON schema for the body of api /record POST endpoint","title":"Interface: RecordsPost"},{"location":"code/interfaces/DocApiTypes.RecordsPost/#interface-recordspost","text":"DocApiTypes .RecordsPost JSON schema for the body of api /record POST endpoint","title":"Interface: RecordsPost"},{"location":"code/interfaces/DocApiTypes.RecordsPut/","text":"Interface: RecordsPut # DocApiTypes .RecordsPut JSON schema for the body of api /record PUT endpoint","title":"Interface: RecordsPut"},{"location":"code/interfaces/DocApiTypes.RecordsPut/#interface-recordsput","text":"DocApiTypes .RecordsPut JSON schema for the body of api /record PUT endpoint","title":"Interface: RecordsPut"},{"location":"code/interfaces/DocApiTypes.SqlPost/","text":"Interface: SqlPost # DocApiTypes .SqlPost JSON schema for the body of api /sql POST endpoint","title":"Interface: SqlPost"},{"location":"code/interfaces/DocApiTypes.SqlPost/#interface-sqlpost","text":"DocApiTypes .SqlPost JSON schema for the body of api /sql POST endpoint","title":"Interface: SqlPost"},{"location":"code/interfaces/DocApiTypes.TablePost/","text":"Interface: TablePost # DocApiTypes .TablePost Creating tables requires a list of columns. fields is not accepted because it\u2019s not generally sensible to set the metadata fields on new tables. Hierarchy # ColumnsPost \u21b3 TablePost","title":"Interface: TablePost"},{"location":"code/interfaces/DocApiTypes.TablePost/#interface-tablepost","text":"DocApiTypes .TablePost Creating tables requires a list of columns. fields is not accepted because it\u2019s not generally sensible to set the metadata fields on new tables.","title":"Interface: TablePost"},{"location":"code/interfaces/DocApiTypes.TablePost/#hierarchy","text":"ColumnsPost \u21b3 TablePost","title":"Hierarchy"},{"location":"code/interfaces/GristData.RowRecord/","text":"Interface: RowRecord # GristData .RowRecord Map of column ids to CellValue \u2018s.","title":"Interface: RowRecord"},{"location":"code/interfaces/GristData.RowRecord/#interface-rowrecord","text":"GristData .RowRecord Map of column ids to CellValue \u2018s.","title":"Interface: RowRecord"},{"location":"code/interfaces/GristData.RowRecords/","text":"Interface: RowRecords # GristData .RowRecords Map of column ids to CellValue arrays, where array indexes correspond to rows.","title":"Interface: RowRecords"},{"location":"code/interfaces/GristData.RowRecords/#interface-rowrecords","text":"GristData .RowRecords Map of column ids to CellValue arrays, where array indexes correspond to rows.","title":"Interface: RowRecords"},{"location":"code/interfaces/TableOperations.OpOptions/","text":"Interface: OpOptions # TableOperations .OpOptions General options for table operations. Hierarchy # OpOptions \u21b3 UpsertOptions Table of contents # Properties # parseStrings Properties # parseStrings # \u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true.","title":"Interface: OpOptions"},{"location":"code/interfaces/TableOperations.OpOptions/#interface-opoptions","text":"TableOperations .OpOptions General options for table operations.","title":"Interface: OpOptions"},{"location":"code/interfaces/TableOperations.OpOptions/#hierarchy","text":"OpOptions \u21b3 UpsertOptions","title":"Hierarchy"},{"location":"code/interfaces/TableOperations.OpOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/TableOperations.OpOptions/#properties","text":"parseStrings","title":"Properties"},{"location":"code/interfaces/TableOperations.OpOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/TableOperations.OpOptions/#parsestrings","text":"\u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true.","title":"parseStrings"},{"location":"code/interfaces/TableOperations.TableOperations/","text":"Interface: TableOperations # TableOperations .TableOperations Offer CRUD-style operations on a table. Table of contents # Methods # create destroy getTableId update upsert Methods # create # \u25b8 create ( records , options? ): Promise < MinimalRecord > Create a record or records. Parameters # Name Type records NewRecord options? OpOptions Returns # Promise < MinimalRecord > destroy # \u25b8 destroy ( recordIds ): Promise < void > Delete a record or records. Parameters # Name Type recordIds number | number [] Returns # Promise < void > getTableId # \u25b8 getTableId (): Promise < string > Determine the tableId of the table. Returns # Promise < string > update # \u25b8 update ( records , options? ): Promise < void > Update a record or records. Parameters # Name Type records Record | Record [] options? OpOptions Returns # Promise < void > upsert # \u25b8 upsert ( records , options? ): Promise < void > Add or update a record or records. Parameters # Name Type records AddOrUpdateRecord | AddOrUpdateRecord [] options? UpsertOptions Returns # Promise < void >","title":"Interface: TableOperations"},{"location":"code/interfaces/TableOperations.TableOperations/#interface-tableoperations","text":"TableOperations .TableOperations Offer CRUD-style operations on a table.","title":"Interface: TableOperations"},{"location":"code/interfaces/TableOperations.TableOperations/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/TableOperations.TableOperations/#methods","text":"create destroy getTableId update upsert","title":"Methods"},{"location":"code/interfaces/TableOperations.TableOperations/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/TableOperations.TableOperations/#create","text":"\u25b8 create ( records , options? ): Promise < MinimalRecord > Create a record or records.","title":"create"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters","text":"Name Type records NewRecord options? OpOptions","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns","text":"Promise < MinimalRecord >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#destroy","text":"\u25b8 destroy ( recordIds ): Promise < void > Delete a record or records.","title":"destroy"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters_1","text":"Name Type recordIds number | number []","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_1","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#gettableid","text":"\u25b8 getTableId (): Promise < string > Determine the tableId of the table.","title":"getTableId"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_2","text":"Promise < string >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#update","text":"\u25b8 update ( records , options? ): Promise < void > Update a record or records.","title":"update"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters_2","text":"Name Type records Record | Record [] options? OpOptions","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_3","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#upsert","text":"\u25b8 upsert ( records , options? ): Promise < void > Add or update a record or records.","title":"upsert"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters_3","text":"Name Type records AddOrUpdateRecord | AddOrUpdateRecord [] options? UpsertOptions","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_4","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/TableOperations.UpsertOptions/","text":"Interface: UpsertOptions # TableOperations .UpsertOptions Extra options for upserts. Hierarchy # OpOptions \u21b3 UpsertOptions Table of contents # Properties # add allowEmptyRequire onMany parseStrings update Properties # add # \u2022 Optional add : boolean Permit inserting a record. Defaults to true. allowEmptyRequire # \u2022 Optional allowEmptyRequire : boolean Allow \u201cwildcard\u201d operation. Defaults to false. onMany # \u2022 Optional onMany : \"all\" | \"none\" | \"first\" Whether to update none, one, or all matching records. Defaults to \u201cfirst\u201d. parseStrings # \u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true. Inherited from # OpOptions . parseStrings update # \u2022 Optional update : boolean Permit updating a record. Defaults to true.","title":"Interface: UpsertOptions"},{"location":"code/interfaces/TableOperations.UpsertOptions/#interface-upsertoptions","text":"TableOperations .UpsertOptions Extra options for upserts.","title":"Interface: UpsertOptions"},{"location":"code/interfaces/TableOperations.UpsertOptions/#hierarchy","text":"OpOptions \u21b3 UpsertOptions","title":"Hierarchy"},{"location":"code/interfaces/TableOperations.UpsertOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/TableOperations.UpsertOptions/#properties","text":"add allowEmptyRequire onMany parseStrings update","title":"Properties"},{"location":"code/interfaces/TableOperations.UpsertOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/TableOperations.UpsertOptions/#add","text":"\u2022 Optional add : boolean Permit inserting a record. Defaults to true.","title":"add"},{"location":"code/interfaces/TableOperations.UpsertOptions/#allowemptyrequire","text":"\u2022 Optional allowEmptyRequire : boolean Allow \u201cwildcard\u201d operation. Defaults to false.","title":"allowEmptyRequire"},{"location":"code/interfaces/TableOperations.UpsertOptions/#onmany","text":"\u2022 Optional onMany : \"all\" | \"none\" | \"first\" Whether to update none, one, or all matching records. Defaults to \u201cfirst\u201d.","title":"onMany"},{"location":"code/interfaces/TableOperations.UpsertOptions/#parsestrings","text":"\u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true.","title":"parseStrings"},{"location":"code/interfaces/TableOperations.UpsertOptions/#inherited-from","text":"OpOptions . parseStrings","title":"Inherited from"},{"location":"code/interfaces/TableOperations.UpsertOptions/#update","text":"\u2022 Optional update : boolean Permit updating a record. Defaults to true.","title":"update"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/","text":"Interface: AccessTokenOptions # grist-plugin-api .AccessTokenOptions Options when creating access tokens. Table of contents # Properties # readOnly Properties # readOnly # \u2022 Optional readOnly : boolean Restrict use of token to reading only","title":"Interface: AccessTokenOptions"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#interface-accesstokenoptions","text":"grist-plugin-api .AccessTokenOptions Options when creating access tokens.","title":"Interface: AccessTokenOptions"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#properties","text":"readOnly","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#readonly","text":"\u2022 Optional readOnly : boolean Restrict use of token to reading only","title":"readOnly"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/","text":"Interface: AccessTokenResult # grist-plugin-api .AccessTokenResult Access token information, including the token string itself, a base URL for API calls for which the access token can be used, and the time-to-live the token was created with. Table of contents # Properties # baseUrl token ttlMsecs Properties # baseUrl # \u2022 baseUrl : string The base url of the API for which the token can be used. Currently tokens are associated with a single document, so the base url will be something like https://..../api/docs/DOCID Access tokens currently only grant access to endpoints dealing with the internal content of a document (such as tables and cells) and not its metadata (such as the document name or who it is shared with). token # \u2022 token : string The token string, which can currently be provided in an api call as a query parameter called \u201cauth\u201d ttlMsecs # \u2022 ttlMsecs : number Number of milliseconds the access token will remain valid for after creation. This will be several minutes.","title":"Interface: AccessTokenResult"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#interface-accesstokenresult","text":"grist-plugin-api .AccessTokenResult Access token information, including the token string itself, a base URL for API calls for which the access token can be used, and the time-to-live the token was created with.","title":"Interface: AccessTokenResult"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#properties","text":"baseUrl token ttlMsecs","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#baseurl","text":"\u2022 baseUrl : string The base url of the API for which the token can be used. Currently tokens are associated with a single document, so the base url will be something like https://..../api/docs/DOCID Access tokens currently only grant access to endpoints dealing with the internal content of a document (such as tables and cells) and not its metadata (such as the document name or who it is shared with).","title":"baseUrl"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#token","text":"\u2022 token : string The token string, which can currently be provided in an api call as a query parameter called \u201cauth\u201d","title":"token"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#ttlmsecs","text":"\u2022 ttlMsecs : number Number of milliseconds the access token will remain valid for after creation. This will be several minutes.","title":"ttlMsecs"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/","text":"Interface: ColumnToMap # grist-plugin-api .ColumnToMap API definitions for CustomSection plugins. Table of contents # Properties # allowMultiple description name optional strictType title type Properties # allowMultiple # \u2022 Optional allowMultiple : boolean Allow multiple column assignment, the result will be list of mapped table column names. description # \u2022 Optional description : null | string Optional long description of a column (used as a help text in section mapping). name # \u2022 name : string Column name that Widget expects. Must be a valid JSON property name. optional # \u2022 Optional optional : boolean Mark column as optional all columns are required by default. strictType # \u2022 Optional strictType : boolean Match column type strictly, so \u201cAny\u201d will require \u201cAny\u201d and not any other type. title # \u2022 Optional title : null | string Title or short description of a column (used as a label in section mapping). type # \u2022 Optional type : string Column types (as comma separated list), by default \u201cAny\u201d, what means that any type is allowed (unless strictType is true).","title":"Interface: ColumnToMap"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#interface-columntomap","text":"grist-plugin-api .ColumnToMap API definitions for CustomSection plugins.","title":"Interface: ColumnToMap"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#properties","text":"allowMultiple description name optional strictType title type","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#allowmultiple","text":"\u2022 Optional allowMultiple : boolean Allow multiple column assignment, the result will be list of mapped table column names.","title":"allowMultiple"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#description","text":"\u2022 Optional description : null | string Optional long description of a column (used as a help text in section mapping).","title":"description"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#name","text":"\u2022 name : string Column name that Widget expects. Must be a valid JSON property name.","title":"name"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#optional","text":"\u2022 Optional optional : boolean Mark column as optional all columns are required by default.","title":"optional"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#stricttype","text":"\u2022 Optional strictType : boolean Match column type strictly, so \u201cAny\u201d will require \u201cAny\u201d and not any other type.","title":"strictType"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#title","text":"\u2022 Optional title : null | string Title or short description of a column (used as a label in section mapping).","title":"title"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#type","text":"\u2022 Optional type : string Column types (as comma separated list), by default \u201cAny\u201d, what means that any type is allowed (unless strictType is true).","title":"type"},{"location":"code/interfaces/grist_plugin_api.CursorPos/","text":"Interface: CursorPos # grist-plugin-api .CursorPos Represents the position of an active cursor on a page. Table of contents # Properties # fieldIndex linkingRowIds rowId rowIndex sectionId Properties # fieldIndex # \u2022 Optional fieldIndex : number The index of the selected field in the current view. linkingRowIds # \u2022 Optional linkingRowIds : UIRowId [] When in a linked section, CursorPos may include which rows in the controlling sections are selected: the rowId in the linking-source section, in that section\u2019s linking source, etc. rowId # \u2022 Optional rowId : UIRowId The rowId (value of the id column) of the current cursor position, or \u2018new\u2019 if the cursor is on a new row. rowIndex # \u2022 Optional rowIndex : number The index of the current row in the current view. sectionId # \u2022 Optional sectionId : number The id of a section that this cursor is in. Ignored when setting a cursor position for a particular view.","title":"Interface: CursorPos"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#interface-cursorpos","text":"grist-plugin-api .CursorPos Represents the position of an active cursor on a page.","title":"Interface: CursorPos"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#properties","text":"fieldIndex linkingRowIds rowId rowIndex sectionId","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#fieldindex","text":"\u2022 Optional fieldIndex : number The index of the selected field in the current view.","title":"fieldIndex"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#linkingrowids","text":"\u2022 Optional linkingRowIds : UIRowId [] When in a linked section, CursorPos may include which rows in the controlling sections are selected: the rowId in the linking-source section, in that section\u2019s linking source, etc.","title":"linkingRowIds"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#rowid","text":"\u2022 Optional rowId : UIRowId The rowId (value of the id column) of the current cursor position, or \u2018new\u2019 if the cursor is on a new row.","title":"rowId"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#rowindex","text":"\u2022 Optional rowIndex : number The index of the current row in the current view.","title":"rowIndex"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#sectionid","text":"\u2022 Optional sectionId : number The id of a section that this cursor is in. Ignored when setting a cursor position for a particular view.","title":"sectionId"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/","text":"Interface: CustomSectionAPI # grist-plugin-api .CustomSectionAPI Interface for the mapping of a custom widget. Table of contents # Methods # configure mappings Methods # configure # \u25b8 configure ( customOptions ): Promise < void > Initial request from a Custom Widget that wants to declare its requirements. Parameters # Name Type customOptions InteractionOptionsRequest Returns # Promise < void > mappings # \u25b8 mappings (): Promise < null | WidgetColumnMap > Returns current widget configuration (if requested through configuration method). Returns # Promise < null | WidgetColumnMap >","title":"Interface: CustomSectionAPI"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#interface-customsectionapi","text":"grist-plugin-api .CustomSectionAPI Interface for the mapping of a custom widget.","title":"Interface: CustomSectionAPI"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#methods","text":"configure mappings","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#configure","text":"\u25b8 configure ( customOptions ): Promise < void > Initial request from a Custom Widget that wants to declare its requirements.","title":"configure"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#parameters","text":"Name Type customOptions InteractionOptionsRequest","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#mappings","text":"\u25b8 mappings (): Promise < null | WidgetColumnMap > Returns current widget configuration (if requested through configuration method).","title":"mappings"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#returns_1","text":"Promise < null | WidgetColumnMap >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/","text":"Interface: FetchSelectedOptions # grist-plugin-api .FetchSelectedOptions Options for functions which fetch data from the selected table or record: onRecords onRecord fetchSelectedRecord fetchSelectedTable GristView.fetchSelectedRecord GristView.fetchSelectedTable The different methods have different default values for keepEncoded and format . Table of contents # Properties # format includeColumns keepEncoded Properties # format # \u2022 Optional format : \"columns\" | \"rows\" rows , the returned data will be an array of objects, one per row, with column names as keys. columns , the returned data will be an object with column names as keys, and arrays of values. includeColumns # \u2022 Optional includeColumns : \"shown\" | \"normal\" | \"all\" shown (default): return only columns that are explicitly shown in the right panel configuration of the widget. This is the only value that doesn\u2019t require full access. normal : return all \u2018normal\u2019 columns, regardless of whether the user has shown them. all : also return special invisible columns like manualSort and display helper columns. keepEncoded # \u2022 Optional keepEncoded : boolean true : the returned data will contain raw CellValue \u2018s. false : the values will be decoded, replacing e.g. ['D', timestamp] with a moment date.","title":"Interface: FetchSelectedOptions"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#interface-fetchselectedoptions","text":"grist-plugin-api .FetchSelectedOptions Options for functions which fetch data from the selected table or record: onRecords onRecord fetchSelectedRecord fetchSelectedTable GristView.fetchSelectedRecord GristView.fetchSelectedTable The different methods have different default values for keepEncoded and format .","title":"Interface: FetchSelectedOptions"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#properties","text":"format includeColumns keepEncoded","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#format","text":"\u2022 Optional format : \"columns\" | \"rows\" rows , the returned data will be an array of objects, one per row, with column names as keys. columns , the returned data will be an object with column names as keys, and arrays of values.","title":"format"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#includecolumns","text":"\u2022 Optional includeColumns : \"shown\" | \"normal\" | \"all\" shown (default): return only columns that are explicitly shown in the right panel configuration of the widget. This is the only value that doesn\u2019t require full access. normal : return all \u2018normal\u2019 columns, regardless of whether the user has shown them. all : also return special invisible columns like manualSort and display helper columns.","title":"includeColumns"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#keepencoded","text":"\u2022 Optional keepEncoded : boolean true : the returned data will contain raw CellValue \u2018s. false : the values will be decoded, replacing e.g. ['D', timestamp] with a moment date.","title":"keepEncoded"},{"location":"code/interfaces/grist_plugin_api.GristColumn/","text":"Interface: GristColumn # grist-plugin-api .GristColumn Metadata about a single column.","title":"Interface: GristColumn"},{"location":"code/interfaces/grist_plugin_api.GristColumn/#interface-gristcolumn","text":"grist-plugin-api .GristColumn Metadata about a single column.","title":"Interface: GristColumn"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/","text":"Interface: GristDocAPI # grist-plugin-api .GristDocAPI Allows getting information from and interacting with the Grist document to which a plugin or widget is attached. Table of contents # Methods # applyUserActions fetchTable getAccessToken getDocName listTables Methods # applyUserActions # \u25b8 applyUserActions ( actions , options? ): Promise < any > Applies an array of user actions. Parameters # Name Type actions any [][] options? any Returns # Promise < any > fetchTable # \u25b8 fetchTable ( tableId ): Promise < any > Returns a complete table of data as GristData.RowRecords , including the \u2018id\u2019 column. Do not modify the returned arrays in-place, especially if used directly (not over RPC). Parameters # Name Type tableId string Returns # Promise < any > getAccessToken # \u25b8 getAccessToken ( options ): Promise < AccessTokenResult > Get a token for out-of-band access to the document. Parameters # Name Type options AccessTokenOptions Returns # Promise < AccessTokenResult > getDocName # \u25b8 getDocName (): Promise < string > Returns an identifier for the document. Returns # Promise < string > listTables # \u25b8 listTables (): Promise < string []> Returns a sorted list of table IDs. Returns # Promise < string []>","title":"Interface: GristDocAPI"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#interface-gristdocapi","text":"grist-plugin-api .GristDocAPI Allows getting information from and interacting with the Grist document to which a plugin or widget is attached.","title":"Interface: GristDocAPI"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#methods","text":"applyUserActions fetchTable getAccessToken getDocName listTables","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#applyuseractions","text":"\u25b8 applyUserActions ( actions , options? ): Promise < any > Applies an array of user actions.","title":"applyUserActions"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#parameters","text":"Name Type actions any [][] options? any","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#fetchtable","text":"\u25b8 fetchTable ( tableId ): Promise < any > Returns a complete table of data as GristData.RowRecords , including the \u2018id\u2019 column. Do not modify the returned arrays in-place, especially if used directly (not over RPC).","title":"fetchTable"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#parameters_1","text":"Name Type tableId string","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_1","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#getaccesstoken","text":"\u25b8 getAccessToken ( options ): Promise < AccessTokenResult > Get a token for out-of-band access to the document.","title":"getAccessToken"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#parameters_2","text":"Name Type options AccessTokenOptions","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_2","text":"Promise < AccessTokenResult >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#getdocname","text":"\u25b8 getDocName (): Promise < string > Returns an identifier for the document.","title":"getDocName"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_3","text":"Promise < string >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#listtables","text":"\u25b8 listTables (): Promise < string []> Returns a sorted list of table IDs.","title":"listTables"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_4","text":"Promise < string []>","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristTable/","text":"Interface: GristTable # grist-plugin-api .GristTable Metadata and data for a table.","title":"Interface: GristTable"},{"location":"code/interfaces/grist_plugin_api.GristTable/#interface-gristtable","text":"grist-plugin-api .GristTable Metadata and data for a table.","title":"Interface: GristTable"},{"location":"code/interfaces/grist_plugin_api.GristView/","text":"Interface: GristView # grist-plugin-api .GristView Interface for the data backing a single widget. Table of contents # Methods # allowSelectBy fetchSelectedRecord fetchSelectedTable setCursorPos setSelectedRows Methods # allowSelectBy # \u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message. Returns # Promise < void > fetchSelectedRecord # \u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Fetches selected record by its rowId . By default, options.keepEncoded is true . Parameters # Name Type rowId number options? FetchSelectedOptions Returns # Promise < any > fetchSelectedTable # \u25b8 fetchSelectedTable ( options? ): Promise < any > Like GristDocAPI.fetchTable , but gets data for the custom section specifically, if there is any. By default, options.keepEncoded is true and format is columns . Parameters # Name Type options? FetchSelectedOptions Returns # Promise < any > setCursorPos # \u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking. Parameters # Name Type pos CursorPos Returns # Promise < void > setSelectedRows # \u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget. Parameters # Name Type rowIds null | number [] Returns # Promise < void >","title":"Interface: GristView"},{"location":"code/interfaces/grist_plugin_api.GristView/#interface-gristview","text":"grist-plugin-api .GristView Interface for the data backing a single widget.","title":"Interface: GristView"},{"location":"code/interfaces/grist_plugin_api.GristView/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.GristView/#methods","text":"allowSelectBy fetchSelectedRecord fetchSelectedTable setCursorPos setSelectedRows","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristView/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristView/#allowselectby","text":"\u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message.","title":"allowSelectBy"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#fetchselectedrecord","text":"\u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Fetches selected record by its rowId . By default, options.keepEncoded is true .","title":"fetchSelectedRecord"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters","text":"Name Type rowId number options? FetchSelectedOptions","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_1","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#fetchselectedtable","text":"\u25b8 fetchSelectedTable ( options? ): Promise < any > Like GristDocAPI.fetchTable , but gets data for the custom section specifically, if there is any. By default, options.keepEncoded is true and format is columns .","title":"fetchSelectedTable"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters_1","text":"Name Type options? FetchSelectedOptions","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_2","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#setcursorpos","text":"\u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking.","title":"setCursorPos"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters_2","text":"Name Type pos CursorPos","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_3","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#setselectedrows","text":"\u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget.","title":"setSelectedRows"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters_3","text":"Name Type rowIds null | number []","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_4","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/","text":"Interface: InteractionOptions # grist-plugin-api .InteractionOptions Widget configuration set and approved by Grist, sent as part of ready message. Table of contents # Properties # accessLevel Properties # accessLevel # \u2022 accessLevel : string Granted access level.","title":"Interface: InteractionOptions"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#interface-interactionoptions","text":"grist-plugin-api .InteractionOptions Widget configuration set and approved by Grist, sent as part of ready message.","title":"Interface: InteractionOptions"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#properties","text":"accessLevel","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#accesslevel","text":"\u2022 accessLevel : string Granted access level.","title":"accessLevel"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/","text":"Interface: InteractionOptionsRequest # grist-plugin-api .InteractionOptionsRequest Initial message sent by the CustomWidget with initial requirements. Table of contents # Properties # allowSelectBy columns hasCustomOptions requiredAccess Properties # allowSelectBy # \u2022 Optional allowSelectBy : boolean Show widget as linking source. columns # \u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget. hasCustomOptions # \u2022 Optional hasCustomOptions : boolean Instructs Grist to show additional menu options that will trigger onEditOptions callback, that Widget can use to show custom options screen. requiredAccess # \u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level.","title":"Interface: InteractionOptionsRequest"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#interface-interactionoptionsrequest","text":"grist-plugin-api .InteractionOptionsRequest Initial message sent by the CustomWidget with initial requirements.","title":"Interface: InteractionOptionsRequest"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#properties","text":"allowSelectBy columns hasCustomOptions requiredAccess","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#allowselectby","text":"\u2022 Optional allowSelectBy : boolean Show widget as linking source.","title":"allowSelectBy"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#columns","text":"\u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget.","title":"columns"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#hascustomoptions","text":"\u2022 Optional hasCustomOptions : boolean Instructs Grist to show additional menu options that will trigger onEditOptions callback, that Widget can use to show custom options screen.","title":"hasCustomOptions"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#requiredaccess","text":"\u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level.","title":"requiredAccess"},{"location":"code/interfaces/grist_plugin_api.ParseOptionSchema/","text":"Interface: ParseOptionSchema # grist-plugin-api .ParseOptionSchema ParseOptionSchema contains information for generaing parse options UI","title":"Interface: ParseOptionSchema"},{"location":"code/interfaces/grist_plugin_api.ParseOptionSchema/#interface-parseoptionschema","text":"grist-plugin-api .ParseOptionSchema ParseOptionSchema contains information for generaing parse options UI","title":"Interface: ParseOptionSchema"},{"location":"code/interfaces/grist_plugin_api.ParseOptions/","text":"Interface: ParseOptions # grist-plugin-api .ParseOptions ParseOptions contains parse options depending on plugin, number of rows, which is special option that can be used for any plugin and schema for generating parse options UI","title":"Interface: ParseOptions"},{"location":"code/interfaces/grist_plugin_api.ParseOptions/#interface-parseoptions","text":"grist-plugin-api .ParseOptions ParseOptions contains parse options depending on plugin, number of rows, which is special option that can be used for any plugin and schema for generating parse options UI","title":"Interface: ParseOptions"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/","text":"Interface: ReadyPayload # grist-plugin-api .ReadyPayload Options when initializing connection to Grist. Hierarchy # Omit < InteractionOptionsRequest , \"hasCustomOptions\" > \u21b3 ReadyPayload Table of contents # Properties # allowSelectBy columns onEditOptions requiredAccess Properties # allowSelectBy # \u2022 Optional allowSelectBy : boolean Show widget as linking source. Inherited from # Omit.allowSelectBy columns # \u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget. Inherited from # Omit.columns onEditOptions # \u2022 Optional onEditOptions : () => unknown Type declaration # \u25b8 (): unknown Handler that will be called by Grist to open additional configuration panel inside the Custom Widget. Returns # unknown requiredAccess # \u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level. Inherited from # Omit.requiredAccess","title":"Interface: ReadyPayload"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#interface-readypayload","text":"grist-plugin-api .ReadyPayload Options when initializing connection to Grist.","title":"Interface: ReadyPayload"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#hierarchy","text":"Omit < InteractionOptionsRequest , \"hasCustomOptions\" > \u21b3 ReadyPayload","title":"Hierarchy"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#properties","text":"allowSelectBy columns onEditOptions requiredAccess","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#allowselectby","text":"\u2022 Optional allowSelectBy : boolean Show widget as linking source.","title":"allowSelectBy"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#inherited-from","text":"Omit.allowSelectBy","title":"Inherited from"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#columns","text":"\u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget.","title":"columns"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#inherited-from_1","text":"Omit.columns","title":"Inherited from"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#oneditoptions","text":"\u2022 Optional onEditOptions : () => unknown","title":"onEditOptions"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#type-declaration","text":"\u25b8 (): unknown Handler that will be called by Grist to open additional configuration panel inside the Custom Widget.","title":"Type declaration"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#returns","text":"unknown","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#requiredaccess","text":"\u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level.","title":"requiredAccess"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#inherited-from_2","text":"Omit.requiredAccess","title":"Inherited from"},{"location":"code/interfaces/grist_plugin_api.RenderOptions/","text":"Interface: RenderOptions # grist-plugin-api .RenderOptions Options for the grist.render function.","title":"Interface: RenderOptions"},{"location":"code/interfaces/grist_plugin_api.RenderOptions/#interface-renderoptions","text":"grist-plugin-api .RenderOptions Options for the grist.render function.","title":"Interface: RenderOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/","text":"Interface: WidgetAPI # grist-plugin-api .WidgetAPI API to manage Custom Widget state. Table of contents # Methods # clearOptions getOption getOptions setOption setOptions Methods # clearOptions # \u25b8 clearOptions (): Promise < void > Clears all the options. Returns # Promise < void > getOption # \u25b8 getOption ( key ): Promise < any > Get single value from Widget options object. Parameters # Name Type key string Returns # Promise < any > getOptions # \u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object. Returns # Promise < null | object > setOption # \u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary). Parameters # Name Type key string value any Returns # Promise < void > setOptions # \u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget. Parameters # Name Type options Object Returns # Promise < void >","title":"Interface: WidgetAPI"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#interface-widgetapi","text":"grist-plugin-api .WidgetAPI API to manage Custom Widget state.","title":"Interface: WidgetAPI"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#methods","text":"clearOptions getOption getOptions setOption setOptions","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#clearoptions","text":"\u25b8 clearOptions (): Promise < void > Clears all the options.","title":"clearOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#getoption","text":"\u25b8 getOption ( key ): Promise < any > Get single value from Widget options object.","title":"getOption"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#parameters","text":"Name Type key string","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_1","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#getoptions","text":"\u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object.","title":"getOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_2","text":"Promise < null | object >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#setoption","text":"\u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary).","title":"setOption"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#parameters_1","text":"Name Type key string value any","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_3","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#setoptions","text":"\u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget.","title":"setOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#parameters_2","text":"Name Type options Object","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_4","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetColumnMap/","text":"Interface: WidgetColumnMap # grist-plugin-api .WidgetColumnMap Current columns mapping between viewFields in section and Custom widget.","title":"Interface: WidgetColumnMap"},{"location":"code/interfaces/grist_plugin_api.WidgetColumnMap/#interface-widgetcolumnmap","text":"grist-plugin-api .WidgetColumnMap Current columns mapping between viewFields in section and Custom widget.","title":"Interface: WidgetColumnMap"},{"location":"code/modules/DocApiTypes/","text":"Module: DocApiTypes # Table of contents # Interfaces # AddOrUpdateRecord MinimalRecord NewRecord Record RecordsPatch RecordsPost RecordsPut SqlPost TablePost","title":"Module: DocApiTypes"},{"location":"code/modules/DocApiTypes/#module-docapitypes","text":"","title":"Module: DocApiTypes"},{"location":"code/modules/DocApiTypes/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/DocApiTypes/#interfaces","text":"AddOrUpdateRecord MinimalRecord NewRecord Record RecordsPatch RecordsPost RecordsPut SqlPost TablePost","title":"Interfaces"},{"location":"code/modules/GristData/","text":"Module: GristData # Table of contents # Enumerations # GristObjCode Interfaces # RowRecord RowRecords Type Aliases # CellValue Type Aliases # CellValue # \u01ac CellValue : number | string | boolean | null | [ GristObjCode , \u2026unknown[]] Possible types of cell content. Each CellValue may either be a primitive (e.g. true , 123 , \"hello\" , null ) or a tuple (JavaScript Array) representing a Grist object. The first element of the tuple is a string character representing the object code. For example, [\"L\", \"foo\", \"bar\"] is a CellValue of a Choice List column, where \"L\" is the type, and \"foo\" and \"bar\" are the choices. Grist Object Types # Code Type L List, e.g. [\"L\", \"foo\", \"bar\"] or [\"L\", 1, 2] l LookUp, as [\"l\", value, options] O Dict, as [\"O\", {key: value, ...}] D DateTimes, as [\"D\", timestamp, timezone] , e.g. [\"D\", 1704945919, \"UTC\"] d Date, as [\"d\", timestamp] , e.g. [\"d\", 1704844800] C Censored, as [\"C\"] R Reference, as [\"R\", table_id, row_id] , e.g. [\"R\", \"People\", 17] r ReferenceList, as [\"r\", table_id, row_id_list] , e.g. [\"r\", \"People\", [1,2]] E Exception, as [\"E\", name, ...] , e.g. [\"E\", \"ValueError\"] P Pending, as [\"P\"] U Unmarshallable, as [\"U\", text_representation] V Version, as [\"V\", version_obj]","title":"Module: GristData"},{"location":"code/modules/GristData/#module-gristdata","text":"","title":"Module: GristData"},{"location":"code/modules/GristData/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/GristData/#enumerations","text":"GristObjCode","title":"Enumerations"},{"location":"code/modules/GristData/#interfaces","text":"RowRecord RowRecords","title":"Interfaces"},{"location":"code/modules/GristData/#type-aliases","text":"CellValue","title":"Type Aliases"},{"location":"code/modules/GristData/#type-aliases_1","text":"","title":"Type Aliases"},{"location":"code/modules/GristData/#cellvalue","text":"\u01ac CellValue : number | string | boolean | null | [ GristObjCode , \u2026unknown[]] Possible types of cell content. Each CellValue may either be a primitive (e.g. true , 123 , \"hello\" , null ) or a tuple (JavaScript Array) representing a Grist object. The first element of the tuple is a string character representing the object code. For example, [\"L\", \"foo\", \"bar\"] is a CellValue of a Choice List column, where \"L\" is the type, and \"foo\" and \"bar\" are the choices.","title":"CellValue"},{"location":"code/modules/GristData/#grist-object-types","text":"Code Type L List, e.g. [\"L\", \"foo\", \"bar\"] or [\"L\", 1, 2] l LookUp, as [\"l\", value, options] O Dict, as [\"O\", {key: value, ...}] D DateTimes, as [\"D\", timestamp, timezone] , e.g. [\"D\", 1704945919, \"UTC\"] d Date, as [\"d\", timestamp] , e.g. [\"d\", 1704844800] C Censored, as [\"C\"] R Reference, as [\"R\", table_id, row_id] , e.g. [\"R\", \"People\", 17] r ReferenceList, as [\"r\", table_id, row_id_list] , e.g. [\"r\", \"People\", [1,2]] E Exception, as [\"E\", name, ...] , e.g. [\"E\", \"ValueError\"] P Pending, as [\"P\"] U Unmarshallable, as [\"U\", text_representation] V Version, as [\"V\", version_obj]","title":"Grist Object Types"},{"location":"code/modules/TableOperations/","text":"Module: TableOperations # Table of contents # Interfaces # OpOptions TableOperations UpsertOptions","title":"Module: TableOperations"},{"location":"code/modules/TableOperations/#module-tableoperations","text":"","title":"Module: TableOperations"},{"location":"code/modules/TableOperations/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/TableOperations/#interfaces","text":"OpOptions TableOperations UpsertOptions","title":"Interfaces"},{"location":"code/modules/grist_plugin_api/","text":"Module: grist-plugin-api # Table of contents # Interfaces # AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap Type Aliases # ColumnsToMap UIRowId Variables # checkers docApi sectionApi selectedTable viewApi widgetApi Functions # allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows Type Aliases # ColumnsToMap # \u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget. UIRowId # \u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row. Variables # checkers # \u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above. docApi # \u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default. sectionApi # \u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget. selectedTable # \u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets). viewApi # \u2022 Const viewApi : GristView Interface for the records backing a custom widget. widgetApi # \u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget. Functions # allowSelectBy # \u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message. Returns # Promise < void > clearOptions # \u25b8 clearOptions (): Promise < void > Clears all the options. Returns # Promise < void > fetchSelectedRecord # \u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default. Parameters # Name Type rowId number options FetchSelectedOptions Returns # Promise < any > fetchSelectedTable # \u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default. Parameters # Name Type options FetchSelectedOptions Returns # Promise < any > getAccessToken # \u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); }); Parameters # Name Type options? AccessTokenOptions Returns # Promise < AccessTokenResult > getOption # \u25b8 getOption ( key ): Promise < any > Get single value from Widget options object. Parameters # Name Type key string Returns # Promise < any > getOptions # \u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object. Returns # Promise < null | object > getTable # \u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted. Parameters # Name Type tableId? string Returns # TableOperations mapColumnNames # \u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean Returns # any mapColumnNamesBack # \u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap Returns # any on # \u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101 Parameters # Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function Returns # Rpc onNewRecord # \u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected. Parameters # Name Type callback ( mappings : null | WidgetColumnMap ) => unknown Returns # void onOptions # \u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it. Parameters # Name Type callback ( options : any , settings : InteractionOptions ) => unknown Returns # void onRecord # \u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false . Parameters # Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void onRecords # \u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false . Parameters # Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void ready # \u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called. Parameters # Name Type settings? ReadyPayload Returns # void setCursorPos # \u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking. Parameters # Name Type pos CursorPos Returns # Promise < void > setOption # \u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary). Parameters # Name Type key string value any Returns # Promise < void > setOptions # \u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget. Parameters # Name Type options Object Returns # Promise < void > setSelectedRows # \u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget. Parameters # Name Type rowIds null | number [] Returns # Promise < void >","title":"grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#module-grist-plugin-api","text":"","title":"Module: grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/grist_plugin_api/#interfaces","text":"AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap","title":"Interfaces"},{"location":"code/modules/grist_plugin_api/#type-aliases","text":"ColumnsToMap UIRowId","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#variables","text":"checkers docApi sectionApi selectedTable viewApi widgetApi","title":"Variables"},{"location":"code/modules/grist_plugin_api/#functions","text":"allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows","title":"Functions"},{"location":"code/modules/grist_plugin_api/#type-aliases_1","text":"","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#columnstomap","text":"\u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget.","title":"ColumnsToMap"},{"location":"code/modules/grist_plugin_api/#uirowid","text":"\u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row.","title":"UIRowId"},{"location":"code/modules/grist_plugin_api/#variables_1","text":"","title":"Variables"},{"location":"code/modules/grist_plugin_api/#checkers","text":"\u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above.","title":"checkers"},{"location":"code/modules/grist_plugin_api/#docapi","text":"\u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default.","title":"docApi"},{"location":"code/modules/grist_plugin_api/#sectionapi","text":"\u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget.","title":"sectionApi"},{"location":"code/modules/grist_plugin_api/#selectedtable","text":"\u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets).","title":"selectedTable"},{"location":"code/modules/grist_plugin_api/#viewapi","text":"\u2022 Const viewApi : GristView Interface for the records backing a custom widget.","title":"viewApi"},{"location":"code/modules/grist_plugin_api/#widgetapi","text":"\u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget.","title":"widgetApi"},{"location":"code/modules/grist_plugin_api/#functions_1","text":"","title":"Functions"},{"location":"code/modules/grist_plugin_api/#allowselectby","text":"\u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message.","title":"allowSelectBy"},{"location":"code/modules/grist_plugin_api/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#clearoptions","text":"\u25b8 clearOptions (): Promise < void > Clears all the options.","title":"clearOptions"},{"location":"code/modules/grist_plugin_api/#returns_1","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedrecord","text":"\u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default.","title":"fetchSelectedRecord"},{"location":"code/modules/grist_plugin_api/#parameters","text":"Name Type rowId number options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_2","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedtable","text":"\u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default.","title":"fetchSelectedTable"},{"location":"code/modules/grist_plugin_api/#parameters_1","text":"Name Type options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_3","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getaccesstoken","text":"\u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); });","title":"getAccessToken"},{"location":"code/modules/grist_plugin_api/#parameters_2","text":"Name Type options? AccessTokenOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_4","text":"Promise < AccessTokenResult >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoption","text":"\u25b8 getOption ( key ): Promise < any > Get single value from Widget options object.","title":"getOption"},{"location":"code/modules/grist_plugin_api/#parameters_3","text":"Name Type key string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_5","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoptions","text":"\u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object.","title":"getOptions"},{"location":"code/modules/grist_plugin_api/#returns_6","text":"Promise < null | object >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#gettable","text":"\u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted.","title":"getTable"},{"location":"code/modules/grist_plugin_api/#parameters_4","text":"Name Type tableId? string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_7","text":"TableOperations","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnames","text":"\u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping.","title":"mapColumnNames"},{"location":"code/modules/grist_plugin_api/#parameters_5","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_8","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnamesback","text":"\u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically.","title":"mapColumnNamesBack"},{"location":"code/modules/grist_plugin_api/#parameters_6","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_9","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#on","text":"\u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101","title":"on"},{"location":"code/modules/grist_plugin_api/#parameters_7","text":"Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_10","text":"Rpc","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onnewrecord","text":"\u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected.","title":"onNewRecord"},{"location":"code/modules/grist_plugin_api/#parameters_8","text":"Name Type callback ( mappings : null | WidgetColumnMap ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_11","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onoptions","text":"\u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it.","title":"onOptions"},{"location":"code/modules/grist_plugin_api/#parameters_9","text":"Name Type callback ( options : any , settings : InteractionOptions ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_12","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecord","text":"\u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false .","title":"onRecord"},{"location":"code/modules/grist_plugin_api/#parameters_10","text":"Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_13","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecords","text":"\u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false .","title":"onRecords"},{"location":"code/modules/grist_plugin_api/#parameters_11","text":"Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_14","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#ready","text":"\u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called.","title":"ready"},{"location":"code/modules/grist_plugin_api/#parameters_12","text":"Name Type settings? ReadyPayload","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_15","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setcursorpos","text":"\u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking.","title":"setCursorPos"},{"location":"code/modules/grist_plugin_api/#parameters_13","text":"Name Type pos CursorPos","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_16","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoption","text":"\u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary).","title":"setOption"},{"location":"code/modules/grist_plugin_api/#parameters_14","text":"Name Type key string value any","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_17","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoptions","text":"\u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget.","title":"setOptions"},{"location":"code/modules/grist_plugin_api/#parameters_15","text":"Name Type options Object","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_18","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setselectedrows","text":"\u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget.","title":"setSelectedRows"},{"location":"code/modules/grist_plugin_api/#parameters_16","text":"Name Type rowIds null | number []","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_19","text":"Promise < void >","title":"Returns"},{"location":"examples/2020-06-book-club/","text":"Book Lists with Library and Store Look-ups # If there\u2019s one thing better than reading a book, it is reading with friends. To organize a book club is pretty simple. The club will work best if everyone actually gets the book, and has opinions for what to read next time. Grist can help with all that. To nudge the book into everyone\u2019s hands, it helps to have links to borrow the book at your local library, and to buy the book from your local (or not-so-local) store. And for ideas about what to read next time, it helps to be able to easily suggest books, and to learn about books others suggest via sites like GoodReads, Wikipedia, and Amazon. Suppose we start with a simple table of books, with book titles and author names. To borrow or buy the book, it is best to have its unique ISBN code, so there\u2019s no ambiguity or mix-ups. One thing we can do is add a link to search for a book on isbnsearch.org by its title and author. To do this, add a new column, and then set it to be a HyperLink column : What we\u2019re going to do is fill this column using a formula that takes the book title and the author\u2019s name, and uses them as a keyword to search on the isbnsearch.org site. Here\u2019s the formula: import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") \"[isbn] https://isbnsearch.org/search?s=\" + urllib.quote(keywords) URLs with spaces or apostrophes or other odd letters you might find in names need special encoding, so we\u2019ve used the python urllib module to make sure everything gets encoded correctly. Now we\u2019ve got a handy [isbn] link beside each book: We can click the arrow by [isbn] , locate the book, and make note of its ISBN code in a new column: Then we can add it into our book list. Now that we have a helping hand for finding ISBNs, let\u2019s add a few more books too: Library and store lookups # Once we have the ISBN, adding a link to buy the book is easy. Here\u2019s a formula for the indiebound.org site, which in the U.S. is likely to have an independent book store near you as a member: \"[buy] https://indiebound.org/book/\" + $ISBN If you don\u2019t have a local bookstore, there\u2019s Amazon, or pretty much any site you like (just find a search page on their site, and match the pattern). import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") + \" \" + ($ISBN or \"\") \"[amazon] https://www.amazon.com/s?i=stripbooks&k=\" + urllib.quote($keywords) The new links look like this: Clicking on the [buy] link near a book now brings us to that book on indiebound.org . For your local library, the same idea is very likely to work. For example, in northern New Jersey, in the US, here\u2019s what you want: import urllib prefix = \"[borrow] https://catalog.bccls.org/polaris/search/searchresults.aspx?ctx=placeholder&type=Keyword&by=ISBN&term=\" prefix + urllib.quote($ISBN) And here\u2019s how to look up Goodreads to see what people think of a book: \"[opinion] https://www.goodreads.com/search?q=\" + $ISBN And Wikipedia to start a deep dive: import urllib keywords = ($Title or \"\") + \" \" + ($Author.Name or \"\") \"[wikipedia] https://en.wikipedia.org/wiki/Special:Search/\" + urllib.quote(keywords) Once we have all these links, it makes sense to add a Card View so we can lay them out: Ready-made template # Here is an example book list that you can play with. The actual books listed may not be your thing, of course! Adjust to taste.","title":"Book club links"},{"location":"examples/2020-06-book-club/#book-lists-with-library-and-store-look-ups","text":"If there\u2019s one thing better than reading a book, it is reading with friends. To organize a book club is pretty simple. The club will work best if everyone actually gets the book, and has opinions for what to read next time. Grist can help with all that. To nudge the book into everyone\u2019s hands, it helps to have links to borrow the book at your local library, and to buy the book from your local (or not-so-local) store. And for ideas about what to read next time, it helps to be able to easily suggest books, and to learn about books others suggest via sites like GoodReads, Wikipedia, and Amazon. Suppose we start with a simple table of books, with book titles and author names. To borrow or buy the book, it is best to have its unique ISBN code, so there\u2019s no ambiguity or mix-ups. One thing we can do is add a link to search for a book on isbnsearch.org by its title and author. To do this, add a new column, and then set it to be a HyperLink column : What we\u2019re going to do is fill this column using a formula that takes the book title and the author\u2019s name, and uses them as a keyword to search on the isbnsearch.org site. Here\u2019s the formula: import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") \"[isbn] https://isbnsearch.org/search?s=\" + urllib.quote(keywords) URLs with spaces or apostrophes or other odd letters you might find in names need special encoding, so we\u2019ve used the python urllib module to make sure everything gets encoded correctly. Now we\u2019ve got a handy [isbn] link beside each book: We can click the arrow by [isbn] , locate the book, and make note of its ISBN code in a new column: Then we can add it into our book list. Now that we have a helping hand for finding ISBNs, let\u2019s add a few more books too:","title":"Book Lists with Library and Store Look-ups"},{"location":"examples/2020-06-book-club/#library-and-store-lookups","text":"Once we have the ISBN, adding a link to buy the book is easy. Here\u2019s a formula for the indiebound.org site, which in the U.S. is likely to have an independent book store near you as a member: \"[buy] https://indiebound.org/book/\" + $ISBN If you don\u2019t have a local bookstore, there\u2019s Amazon, or pretty much any site you like (just find a search page on their site, and match the pattern). import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") + \" \" + ($ISBN or \"\") \"[amazon] https://www.amazon.com/s?i=stripbooks&k=\" + urllib.quote($keywords) The new links look like this: Clicking on the [buy] link near a book now brings us to that book on indiebound.org . For your local library, the same idea is very likely to work. For example, in northern New Jersey, in the US, here\u2019s what you want: import urllib prefix = \"[borrow] https://catalog.bccls.org/polaris/search/searchresults.aspx?ctx=placeholder&type=Keyword&by=ISBN&term=\" prefix + urllib.quote($ISBN) And here\u2019s how to look up Goodreads to see what people think of a book: \"[opinion] https://www.goodreads.com/search?q=\" + $ISBN And Wikipedia to start a deep dive: import urllib keywords = ($Title or \"\") + \" \" + ($Author.Name or \"\") \"[wikipedia] https://en.wikipedia.org/wiki/Special:Search/\" + urllib.quote(keywords) Once we have all these links, it makes sense to add a Card View so we can lay them out:","title":"Library and store lookups"},{"location":"examples/2020-06-book-club/#ready-made-template","text":"Here is an example book list that you can play with. The actual books listed may not be your thing, of course! Adjust to taste.","title":"Ready-made template"},{"location":"examples/2020-06-credit-card/","text":"Slicing and Dicing Expenses # Grist offers a handy way to explore your credit card transactions quickly, for example if you want to see transactions by: Category Card Member Month Combination of any of the above Here is an example using data from American Express (redacted for privacy, of course). See below for using it as a template for your own data. This first page is a dashboard: it shows a few summary charts \u2014 your expenses by month, and two pie charts to let you quickly see which card member or which category has the most expenses. The next three pages show breakdowns by Category, Card Member, and by Month. In addition to the totals by Category, you can click on any Category to see all transactions in it, and then click on any transaction to see its complete details. Here is the example with sample data that you can play with. To use it for your own data, start by downloading your transactions. For American Express credit cards: Log in at https://americanexpress.com Go to \u201cStatements & Activity\u201d tab, then select \u201cView By Year\u201d or \u201cCustom Date Range\u201d. Click \u201cDownload your Transactions\u201d icon. When asked to choose the download format, select \u201cCSV\u201d and select the checkbox that says \u201cInclude all additional transaction details\u201d. To import this data into Grist: Open the template here . Click \u201cAdd New\u201d button and choose \u201cImport from file\u201d. In the dialog that shows, change \u201cTo\u201d table from \u201cNew Table\u201d to \u201cActivity\u201d, like so: Click \u201cImport\u201d, and you are done. Your data is ready to explore. Note When you start with a template link, your copy of the document is initially unsaved. To keep this data for later, click \u201cSave Copy\u201d, and give the document a name. You\u2019ll see the document later on your Grist home page at https://docs.getgrist.com . A template like this doesn\u2019t take long to prepare. It uses a combination of summary tables and widget linking . Have feedback or improvements to contribute? Please share with us by emailing support@getgrist.com .","title":"Credit card expenses"},{"location":"examples/2020-06-credit-card/#slicing-and-dicing-expenses","text":"Grist offers a handy way to explore your credit card transactions quickly, for example if you want to see transactions by: Category Card Member Month Combination of any of the above Here is an example using data from American Express (redacted for privacy, of course). See below for using it as a template for your own data. This first page is a dashboard: it shows a few summary charts \u2014 your expenses by month, and two pie charts to let you quickly see which card member or which category has the most expenses. The next three pages show breakdowns by Category, Card Member, and by Month. In addition to the totals by Category, you can click on any Category to see all transactions in it, and then click on any transaction to see its complete details. Here is the example with sample data that you can play with. To use it for your own data, start by downloading your transactions. For American Express credit cards: Log in at https://americanexpress.com Go to \u201cStatements & Activity\u201d tab, then select \u201cView By Year\u201d or \u201cCustom Date Range\u201d. Click \u201cDownload your Transactions\u201d icon. When asked to choose the download format, select \u201cCSV\u201d and select the checkbox that says \u201cInclude all additional transaction details\u201d. To import this data into Grist: Open the template here . Click \u201cAdd New\u201d button and choose \u201cImport from file\u201d. In the dialog that shows, change \u201cTo\u201d table from \u201cNew Table\u201d to \u201cActivity\u201d, like so: Click \u201cImport\u201d, and you are done. Your data is ready to explore. Note When you start with a template link, your copy of the document is initially unsaved. To keep this data for later, click \u201cSave Copy\u201d, and give the document a name. You\u2019ll see the document later on your Grist home page at https://docs.getgrist.com . A template like this doesn\u2019t take long to prepare. It uses a combination of summary tables and widget linking . Have feedback or improvements to contribute? Please share with us by emailing support@getgrist.com .","title":"Slicing and Dicing Expenses"},{"location":"examples/2020-07-email-compose/","text":"Prepare Emails using Formulas # You may already know that you can add hyperlink fields in Grist. You may also know about \u201cmailto\u201d links that open a mail program to create a new email message. Less well-known is that \u201cmailto\u201d links allow pre-filling many parts of the email message. If you use Grist to store contacts, you can essentially create email templates using Grist formulas. Simple Mailto Links # The simplest \u201cmailto\u201d link in Grist looks like mailto:someone@example.com . When the column is set to Text, and its format is set to Hyperlink, it shows as someone@example.com . If you have a table with columns Full_Name and Email , add another column with this formula: \"Compose Email mailto:%s\" % ($Email) . Set its type to Text, and its cell format to Hyperlink: You\u2019ll get a link in each person\u2019s row, which you can click to start composing an email to that person: See an example of this in action here: Simple Compose . Cc, Bcc, Subject, Body # In addition to opening your email program and filling in the \u201cTO\u201d field, a \u201cmailto\u201d link can fill in other parts of the email, using this format: mailto: from other elements using blank lines. Configuring submission options # You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel. Publishing your form # Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error. Form submissions # After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form"},{"location":"widget-form/#page-widget-form","text":"The form widget allows you to collect data in a form view which populates your Grist data table upon submission.","title":"Page widget: Form"},{"location":"widget-form/#setting-up-your-data","text":"Create a table containing the columns of data you wish to populate via form.","title":"Setting up your data"},{"location":"widget-form/#creating-your-form","text":"Add a form widget from the \u201cAdd New\u201d menu. Select the data table you wish to populate with form data. Then, customize the form to your heart\u2019s desire! By default, the form view will include elements for headers and descriptions as well as all columns (fields) from the underlying data table.","title":"Creating your form"},{"location":"widget-form/#adding-and-removing-elements","text":"To add additional form elements, click the + icon at the bottom of the form. From the menu, you can add the following elements: New Question: Select a column type to create a new field. \u201c\u2022\u2022\u2022 More >\u201d will open an expanded menu listing all column types. Adding a new question will add a new column to the underlying data table. Unmapped Fields: Lists any hidden fields from the underlying data table. Building Blocks: Customize further by adding these additional elements! You can remove any element from the form by hovering over the object and clicking the trash icon to delete. You can hide any uneccessary fields from the form by hovering over the object and clicking the x icon.","title":"Adding and removing elements"},{"location":"widget-form/#configuring-fields","text":"You can provide alternative titles for your form fields, rather than use the same column name from the underlying data table. For example, on our form, we have a toggle that is titled \u201cMay we contact you?\u201d. In the data table, this column is labeled \u201cOk to Contact?\u201d. Field titles can be configured under the \u201cField\u201d tab of the creator panel. To make a form field required, check the box next to \u201cRequired field\u201d. If a user attempts to submit a form without filling in the required field, they will get an alert to fill out the field.","title":"Configuring fields"},{"location":"widget-form/#configuring-building-blocks","text":"Header and Paragraph building blocks can be edited either directly in the block or from the creator panel. In the creator panel, you have text alignment options available. For additional formatting, both elements allow the use of Markdown formatting. For help on Markdown formatting, check out the Markdown Guide . HTML Formatting HTML tags can be used in Markdown-formatted text. Be sure to separate block-level HTML elements like from other elements using blank lines.","title":"Configuring building blocks"},{"location":"widget-form/#configuring-submission-options","text":"You also have the option to configure different settings for the \u201cSubmit\u201d button. You can change the button label, update the success text which appears after a form is submitted and choose to allow multiple form submissions. You also have the option to select a specific URL to redirect to following submission. These options are all available under the \u201cForm\u201d tab and \u201cSubmission\u201d subtab of the creator panel.","title":"Configuring submission options"},{"location":"widget-form/#publishing-your-form","text":"Once you have finished customizing your form, you have the option to preview your form, prior to publishing. Previewing will allow you to see what your form will look like to end users, without making it available for use. After you have confirmed everything is as you wish, you can publish your form. Note that only users with \u201cOWNER\u201d access have permission to publish a form. The first time you publish a form, the following informational popup will appear, explaining the permissions a published form grants. Once a form has been published, a button will appear to copy the link to the form. Share this link with end users to fill out your form! You also have the option to unpublish your form. Note that unpublishing the form will disable the share link. Users accessing the form via that link would then see an error.","title":"Publishing your form"},{"location":"widget-form/#form-submissions","text":"After sharing the link to your published form, end users can submit data to your Grist document via the form. All submitted data will appear in your underlying data table in your Grist document! Collecting data has never been so easy. \ud83d\ude0d","title":"Form submissions"},{"location":"widget-table/","text":"Page widget: Table # The Table widget is a versatile spreadsheet-like grid. Here are some useful features to know. Column operations # Resize columns : Click on the line between column headers, and drag it to resize columns. Reorder columns : With a column selected, drag its header to move it to a different place relative to other columns. (You can also do this by reordering fields in the widget options panel.) Rename columns : With a column selected, click its header to rename it. Hit Enter to save the new name. Add columns : Click the \u201c+\u201d icon on the right of all the column headers to add a new column, or show any of the hidden columns. The column menu also allows inserting a new column next to an existing column, as do the keyboard shortcuts Alt + + (insert before) and Alt + = (insert after). After adding a column, the column name (set by default to \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc.) is immediately selected and highlighted to let you quickly rename it. Just type in the new name and hit Enter , or hit Escape to keep the default name. Hide columns : Move the mouse over the column header and click the triangle to open the column menu. Click \u201cHide column\u201d to hide the column. The column remains in the underlying data, and can be shown again using the \u201c+\u201d icon on the right of the column headers, or the field list in the widget options panel. Delete columns : Delete the actual column of data using the column menu option, or the Alt + Minus keyboard shortcut. The table is the only widget that allows deleting a column. Note: deleting and hiding are different. Hiding a columns removes it only from the current page widget, but leaves it in the data and available to formulas. Deleting a column removes it from everywhere. (Of course, undo still works for either operation!) Row operations # Add rows : Type into the last row in a table, which is highlighted to indicate that it\u2019s a placeholder for adding new records. Right click a row number to insert a blank row next to an existing row, or use the keyboard shortcuts \u2318 \u21e7 = (Mac) or Ctrl + Shift + = (Windows) to insert before, and \u2318 = (Mac) or Ctrl + = (Windows) to insert after. Delete rows : Right click a row number and select the \u201cDelete\u201d option to delete a row, or use the \u2318 + Minus (Mac) or Ctrl + Minus (Windows) shortcut. If you select a range of cells first, either of these delete actions will delete all rows included in the range. Link to rows : Right click a row number and select \u201cCopy anchor link\u201d to copy a link to the selected cell of that row. The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document. Navigation and selection # Navigate using shortcuts : Use keyboard shortcuts to navigate the grid: Tab , Shift + Tab Move to the next or previous column, saving changes if editing a cell. \u2318 + Up (Mac) or Ctrl + Up (Windows) Move up to the first row. \u2318 + Down (Mac) or Ctrl + Down (Windows) Move down to the last row. Home or Fn + \u2190 (Mac) Move to the beginning of a row. End or Fn + \u2192 (Mac) Move to the end of a row. PageDown or Fn + \u2193 (Mac) Move down one page of rows. PageUp or Fn + \u2191 (Mac) Move up one page of rows. Alt + Down , Alt + Up Move down or up five rows. Select ranges : Click and drag a mouse across the grid to select a range of cells to copy (copy and paste using the usual keyboard shortcuts for your computer). Another way to select a range is to click one cell, and then hold Shift while clicking another cell, or while navigating with the arrow keys. Fill down data : Select a range of cells, and hit \u2318 + D (Mac) or Ctrl + D (Windows) to fill the whole selected range with the values of the cells in the top row of the range. Customization # Customize table looks : In the widget options panel, you can turn off horizontal or vertical grid lines, or turn on zebra striping. For example, this lets you change the look of your grid to a list like this:","title":"Table widget"},{"location":"widget-table/#page-widget-table","text":"The Table widget is a versatile spreadsheet-like grid. Here are some useful features to know.","title":"Page widget: Table"},{"location":"widget-table/#column-operations","text":"Resize columns : Click on the line between column headers, and drag it to resize columns. Reorder columns : With a column selected, drag its header to move it to a different place relative to other columns. (You can also do this by reordering fields in the widget options panel.) Rename columns : With a column selected, click its header to rename it. Hit Enter to save the new name. Add columns : Click the \u201c+\u201d icon on the right of all the column headers to add a new column, or show any of the hidden columns. The column menu also allows inserting a new column next to an existing column, as do the keyboard shortcuts Alt + + (insert before) and Alt + = (insert after). After adding a column, the column name (set by default to \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc.) is immediately selected and highlighted to let you quickly rename it. Just type in the new name and hit Enter , or hit Escape to keep the default name. Hide columns : Move the mouse over the column header and click the triangle to open the column menu. Click \u201cHide column\u201d to hide the column. The column remains in the underlying data, and can be shown again using the \u201c+\u201d icon on the right of the column headers, or the field list in the widget options panel. Delete columns : Delete the actual column of data using the column menu option, or the Alt + Minus keyboard shortcut. The table is the only widget that allows deleting a column. Note: deleting and hiding are different. Hiding a columns removes it only from the current page widget, but leaves it in the data and available to formulas. Deleting a column removes it from everywhere. (Of course, undo still works for either operation!)","title":"Column operations"},{"location":"widget-table/#row-operations","text":"Add rows : Type into the last row in a table, which is highlighted to indicate that it\u2019s a placeholder for adding new records. Right click a row number to insert a blank row next to an existing row, or use the keyboard shortcuts \u2318 \u21e7 = (Mac) or Ctrl + Shift + = (Windows) to insert before, and \u2318 = (Mac) or Ctrl + = (Windows) to insert after. Delete rows : Right click a row number and select the \u201cDelete\u201d option to delete a row, or use the \u2318 + Minus (Mac) or Ctrl + Minus (Windows) shortcut. If you select a range of cells first, either of these delete actions will delete all rows included in the range. Link to rows : Right click a row number and select \u201cCopy anchor link\u201d to copy a link to the selected cell of that row. The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document.","title":"Row operations"},{"location":"widget-table/#navigation-and-selection","text":"Navigate using shortcuts : Use keyboard shortcuts to navigate the grid: Tab , Shift + Tab Move to the next or previous column, saving changes if editing a cell. \u2318 + Up (Mac) or Ctrl + Up (Windows) Move up to the first row. \u2318 + Down (Mac) or Ctrl + Down (Windows) Move down to the last row. Home or Fn + \u2190 (Mac) Move to the beginning of a row. End or Fn + \u2192 (Mac) Move to the end of a row. PageDown or Fn + \u2193 (Mac) Move down one page of rows. PageUp or Fn + \u2191 (Mac) Move up one page of rows. Alt + Down , Alt + Up Move down or up five rows. Select ranges : Click and drag a mouse across the grid to select a range of cells to copy (copy and paste using the usual keyboard shortcuts for your computer). Another way to select a range is to click one cell, and then hold Shift while clicking another cell, or while navigating with the arrow keys. Fill down data : Select a range of cells, and hit \u2318 + D (Mac) or Ctrl + D (Windows) to fill the whole selected range with the values of the cells in the top row of the range.","title":"Navigation and selection"},{"location":"widget-table/#customization","text":"Customize table looks : In the widget options panel, you can turn off horizontal or vertical grid lines, or turn on zebra striping. For example, this lets you change the look of your grid to a list like this:","title":"Customization"},{"location":"workspaces/","text":"Workspaces # Documents can be collected in folders called \u201cworkspaces\u201d which can be shared as a single unit with other users. This is convenient, for example, for documents related to a single project. When a site is first created, it has a single workspace called \u201cHome.\u201d You can rename that workspace if you like, by hovering over the workspace name on the left bar, clicking on the three-dots icon, and selecting \u201cRename\u201d. Even better, you can create new workspaces, so you can group your documents and share them as a unit. To add a document to a specific workspace, click on that workspace in the left bar, then select \u201cAdd New\u201d, then \u201cCreate empty document\u201d (or \u201cImport document\u201d). To move a document from one workspace to another, hover over the document, click on the three-dots icon to the right of the document\u2019s name, and select \u201cMove\u201d. You\u2019ll then have the option to pick the workspace you want. Managing access # On team sites , workspace owners can control who has access to a workspace using \u201cManage Users.\u201d The controls are just like for sharing documents . Workspaces in personal sites cannot be shared. Viewers of a team site will also be viewers of all its workspaces. Likewise for editors and owners. To exclude a workspace from inheriting team site viewers, editors, and owners, set \u201cInherit Access\u201d to \u201cNone\u201d. You can also set it to \u201cView Only\u201d to limit inheritance to view rights, or \u201cView & Edit\u201d to limit inheritance to view and edit rights (excluding the right to control sharing options). It is possible to be an editor/owner of a workspace and not be able to open all documents within that workspace. This can happen if an owner of a document limits inheritance to \u201cNone.\u201d You will still see the document listed, so that if you ever decide to delete the workspace you\u2019ll know what you\u2019re deleting. Viewers of a workspace will only see documents they have access to listed.","title":"Workspaces"},{"location":"workspaces/#workspaces","text":"Documents can be collected in folders called \u201cworkspaces\u201d which can be shared as a single unit with other users. This is convenient, for example, for documents related to a single project. When a site is first created, it has a single workspace called \u201cHome.\u201d You can rename that workspace if you like, by hovering over the workspace name on the left bar, clicking on the three-dots icon, and selecting \u201cRename\u201d. Even better, you can create new workspaces, so you can group your documents and share them as a unit. To add a document to a specific workspace, click on that workspace in the left bar, then select \u201cAdd New\u201d, then \u201cCreate empty document\u201d (or \u201cImport document\u201d). To move a document from one workspace to another, hover over the document, click on the three-dots icon to the right of the document\u2019s name, and select \u201cMove\u201d. You\u2019ll then have the option to pick the workspace you want.","title":"Workspaces"},{"location":"workspaces/#managing-access","text":"On team sites , workspace owners can control who has access to a workspace using \u201cManage Users.\u201d The controls are just like for sharing documents . Workspaces in personal sites cannot be shared. Viewers of a team site will also be viewers of all its workspaces. Likewise for editors and owners. To exclude a workspace from inheriting team site viewers, editors, and owners, set \u201cInherit Access\u201d to \u201cNone\u201d. You can also set it to \u201cView Only\u201d to limit inheritance to view rights, or \u201cView & Edit\u201d to limit inheritance to view and edit rights (excluding the right to control sharing options). It is possible to be an editor/owner of a workspace and not be able to open all documents within that workspace. This can happen if an owner of a document limits inheritance to \u201cNone.\u201d You will still see the document listed, so that if you ever decide to delete the workspace you\u2019ll know what you\u2019re deleting. Viewers of a workspace will only see documents they have access to listed.","title":"Managing access"},{"location":"code/","text":"Plugin API # The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Intro to Plugin API"},{"location":"code/#plugin-api","text":"The plugin API is used in custom widgets to interact with the document within which the widget is embedded. If you haven\u2019t already, be sure to read our summary of Custom Widgets and look at the grist-widget repository for examples of widget implementations. Here you can find a reference for functions and interfaces available to custom widgets, starting with the grist object.","title":"Plugin API"},{"location":"code/enums/GristData.GristObjCode/","text":"Enumeration: GristObjCode # GristData .GristObjCode Letter codes for CellValue types encoded as [code, args\u2026] tuples. Table of contents # Enumeration Members # Censored Date DateTime Dict Exception List LookUp Pending Reference ReferenceList Skip Unmarshallable Versions Enumeration Members # Censored # \u2022 Censored = \"C\" Date # \u2022 Date = \"d\" DateTime # \u2022 DateTime = \"D\" Dict # \u2022 Dict = \"O\" Exception # \u2022 Exception = \"E\" List # \u2022 List = \"L\" LookUp # \u2022 LookUp = \"l\" Pending # \u2022 Pending = \"P\" Reference # \u2022 Reference = \"R\" ReferenceList # \u2022 ReferenceList = \"r\" Skip # \u2022 Skip = \"S\" Unmarshallable # \u2022 Unmarshallable = \"U\" Versions # \u2022 Versions = \"V\"","title":"Enumeration: GristObjCode"},{"location":"code/enums/GristData.GristObjCode/#enumeration-gristobjcode","text":"GristData .GristObjCode Letter codes for CellValue types encoded as [code, args\u2026] tuples.","title":"Enumeration: GristObjCode"},{"location":"code/enums/GristData.GristObjCode/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/enums/GristData.GristObjCode/#enumeration-members","text":"Censored Date DateTime Dict Exception List LookUp Pending Reference ReferenceList Skip Unmarshallable Versions","title":"Enumeration Members"},{"location":"code/enums/GristData.GristObjCode/#enumeration-members_1","text":"","title":"Enumeration Members"},{"location":"code/enums/GristData.GristObjCode/#censored","text":"\u2022 Censored = \"C\"","title":"Censored"},{"location":"code/enums/GristData.GristObjCode/#date","text":"\u2022 Date = \"d\"","title":"Date"},{"location":"code/enums/GristData.GristObjCode/#datetime","text":"\u2022 DateTime = \"D\"","title":"DateTime"},{"location":"code/enums/GristData.GristObjCode/#dict","text":"\u2022 Dict = \"O\"","title":"Dict"},{"location":"code/enums/GristData.GristObjCode/#exception","text":"\u2022 Exception = \"E\"","title":"Exception"},{"location":"code/enums/GristData.GristObjCode/#list","text":"\u2022 List = \"L\"","title":"List"},{"location":"code/enums/GristData.GristObjCode/#lookup","text":"\u2022 LookUp = \"l\"","title":"LookUp"},{"location":"code/enums/GristData.GristObjCode/#pending","text":"\u2022 Pending = \"P\"","title":"Pending"},{"location":"code/enums/GristData.GristObjCode/#reference","text":"\u2022 Reference = \"R\"","title":"Reference"},{"location":"code/enums/GristData.GristObjCode/#referencelist","text":"\u2022 ReferenceList = \"r\"","title":"ReferenceList"},{"location":"code/enums/GristData.GristObjCode/#skip","text":"\u2022 Skip = \"S\"","title":"Skip"},{"location":"code/enums/GristData.GristObjCode/#unmarshallable","text":"\u2022 Unmarshallable = \"U\"","title":"Unmarshallable"},{"location":"code/enums/GristData.GristObjCode/#versions","text":"\u2022 Versions = \"V\"","title":"Versions"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/","text":"Interface: AddOrUpdateRecord # DocApiTypes .AddOrUpdateRecord JSON schema for api /record endpoint. Used in PUT method for adding or updating records. Table of contents # Properties # fields require Properties # fields # \u2022 Optional fields : Object The values we will place in particular columns, either overwriting values in an existing record, or setting initial values in a new record. Index signature # \u25aa [coldId: string ]: CellValue require # \u2022 require : { [coldId: string] : CellValue ; } & { id? : number } The values we expect to have in particular columns, either by matching with an existing record, or creating a new record.","title":"Interface: AddOrUpdateRecord"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#interface-addorupdaterecord","text":"DocApiTypes .AddOrUpdateRecord JSON schema for api /record endpoint. Used in PUT method for adding or updating records.","title":"Interface: AddOrUpdateRecord"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#properties","text":"fields require","title":"Properties"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#fields","text":"\u2022 Optional fields : Object The values we will place in particular columns, either overwriting values in an existing record, or setting initial values in a new record.","title":"fields"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#index-signature","text":"\u25aa [coldId: string ]: CellValue","title":"Index signature"},{"location":"code/interfaces/DocApiTypes.AddOrUpdateRecord/#require","text":"\u2022 require : { [coldId: string] : CellValue ; } & { id? : number } The values we expect to have in particular columns, either by matching with an existing record, or creating a new record.","title":"require"},{"location":"code/interfaces/DocApiTypes.MinimalRecord/","text":"Interface: MinimalRecord # DocApiTypes .MinimalRecord The row id of a record, without any of its content.","title":"Interface: MinimalRecord"},{"location":"code/interfaces/DocApiTypes.MinimalRecord/#interface-minimalrecord","text":"DocApiTypes .MinimalRecord The row id of a record, without any of its content.","title":"Interface: MinimalRecord"},{"location":"code/interfaces/DocApiTypes.NewRecord/","text":"Interface: NewRecord # DocApiTypes .NewRecord JSON schema for api /record endpoint. Used in POST method for adding new records. Table of contents # Properties # fields Properties # fields # \u2022 Optional fields : Object Initial values of cells in record. Optional, if not set cells are left blank. Index signature # \u25aa [coldId: string ]: CellValue","title":"Interface: NewRecord"},{"location":"code/interfaces/DocApiTypes.NewRecord/#interface-newrecord","text":"DocApiTypes .NewRecord JSON schema for api /record endpoint. Used in POST method for adding new records.","title":"Interface: NewRecord"},{"location":"code/interfaces/DocApiTypes.NewRecord/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/DocApiTypes.NewRecord/#properties","text":"fields","title":"Properties"},{"location":"code/interfaces/DocApiTypes.NewRecord/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/DocApiTypes.NewRecord/#fields","text":"\u2022 Optional fields : Object Initial values of cells in record. Optional, if not set cells are left blank.","title":"fields"},{"location":"code/interfaces/DocApiTypes.NewRecord/#index-signature","text":"\u25aa [coldId: string ]: CellValue","title":"Index signature"},{"location":"code/interfaces/DocApiTypes.Record/","text":"Interface: Record # DocApiTypes .Record JSON schema for api /record endpoint. Used in PATCH method for updating existing records.","title":"Interface: Record"},{"location":"code/interfaces/DocApiTypes.Record/#interface-record","text":"DocApiTypes .Record JSON schema for api /record endpoint. Used in PATCH method for updating existing records.","title":"Interface: Record"},{"location":"code/interfaces/DocApiTypes.RecordsPatch/","text":"Interface: RecordsPatch # DocApiTypes .RecordsPatch JSON schema for the body of api /record PATCH endpoint","title":"Interface: RecordsPatch"},{"location":"code/interfaces/DocApiTypes.RecordsPatch/#interface-recordspatch","text":"DocApiTypes .RecordsPatch JSON schema for the body of api /record PATCH endpoint","title":"Interface: RecordsPatch"},{"location":"code/interfaces/DocApiTypes.RecordsPost/","text":"Interface: RecordsPost # DocApiTypes .RecordsPost JSON schema for the body of api /record POST endpoint","title":"Interface: RecordsPost"},{"location":"code/interfaces/DocApiTypes.RecordsPost/#interface-recordspost","text":"DocApiTypes .RecordsPost JSON schema for the body of api /record POST endpoint","title":"Interface: RecordsPost"},{"location":"code/interfaces/DocApiTypes.RecordsPut/","text":"Interface: RecordsPut # DocApiTypes .RecordsPut JSON schema for the body of api /record PUT endpoint","title":"Interface: RecordsPut"},{"location":"code/interfaces/DocApiTypes.RecordsPut/#interface-recordsput","text":"DocApiTypes .RecordsPut JSON schema for the body of api /record PUT endpoint","title":"Interface: RecordsPut"},{"location":"code/interfaces/DocApiTypes.SqlPost/","text":"Interface: SqlPost # DocApiTypes .SqlPost JSON schema for the body of api /sql POST endpoint","title":"Interface: SqlPost"},{"location":"code/interfaces/DocApiTypes.SqlPost/#interface-sqlpost","text":"DocApiTypes .SqlPost JSON schema for the body of api /sql POST endpoint","title":"Interface: SqlPost"},{"location":"code/interfaces/DocApiTypes.TablePost/","text":"Interface: TablePost # DocApiTypes .TablePost Creating tables requires a list of columns. fields is not accepted because it\u2019s not generally sensible to set the metadata fields on new tables. Hierarchy # ColumnsPost \u21b3 TablePost","title":"Interface: TablePost"},{"location":"code/interfaces/DocApiTypes.TablePost/#interface-tablepost","text":"DocApiTypes .TablePost Creating tables requires a list of columns. fields is not accepted because it\u2019s not generally sensible to set the metadata fields on new tables.","title":"Interface: TablePost"},{"location":"code/interfaces/DocApiTypes.TablePost/#hierarchy","text":"ColumnsPost \u21b3 TablePost","title":"Hierarchy"},{"location":"code/interfaces/GristData.RowRecord/","text":"Interface: RowRecord # GristData .RowRecord Map of column ids to CellValue \u2018s.","title":"Interface: RowRecord"},{"location":"code/interfaces/GristData.RowRecord/#interface-rowrecord","text":"GristData .RowRecord Map of column ids to CellValue \u2018s.","title":"Interface: RowRecord"},{"location":"code/interfaces/GristData.RowRecords/","text":"Interface: RowRecords # GristData .RowRecords Map of column ids to CellValue arrays, where array indexes correspond to rows.","title":"Interface: RowRecords"},{"location":"code/interfaces/GristData.RowRecords/#interface-rowrecords","text":"GristData .RowRecords Map of column ids to CellValue arrays, where array indexes correspond to rows.","title":"Interface: RowRecords"},{"location":"code/interfaces/TableOperations.OpOptions/","text":"Interface: OpOptions # TableOperations .OpOptions General options for table operations. Hierarchy # OpOptions \u21b3 UpsertOptions Table of contents # Properties # parseStrings Properties # parseStrings # \u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true.","title":"Interface: OpOptions"},{"location":"code/interfaces/TableOperations.OpOptions/#interface-opoptions","text":"TableOperations .OpOptions General options for table operations.","title":"Interface: OpOptions"},{"location":"code/interfaces/TableOperations.OpOptions/#hierarchy","text":"OpOptions \u21b3 UpsertOptions","title":"Hierarchy"},{"location":"code/interfaces/TableOperations.OpOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/TableOperations.OpOptions/#properties","text":"parseStrings","title":"Properties"},{"location":"code/interfaces/TableOperations.OpOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/TableOperations.OpOptions/#parsestrings","text":"\u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true.","title":"parseStrings"},{"location":"code/interfaces/TableOperations.TableOperations/","text":"Interface: TableOperations # TableOperations .TableOperations Offer CRUD-style operations on a table. Table of contents # Methods # create destroy getTableId update upsert Methods # create # \u25b8 create ( records , options? ): Promise < MinimalRecord > Create a record or records. Parameters # Name Type records NewRecord options? OpOptions Returns # Promise < MinimalRecord > destroy # \u25b8 destroy ( recordIds ): Promise < void > Delete a record or records. Parameters # Name Type recordIds number | number [] Returns # Promise < void > getTableId # \u25b8 getTableId (): Promise < string > Determine the tableId of the table. Returns # Promise < string > update # \u25b8 update ( records , options? ): Promise < void > Update a record or records. Parameters # Name Type records Record | Record [] options? OpOptions Returns # Promise < void > upsert # \u25b8 upsert ( records , options? ): Promise < void > Add or update a record or records. Parameters # Name Type records AddOrUpdateRecord | AddOrUpdateRecord [] options? UpsertOptions Returns # Promise < void >","title":"Interface: TableOperations"},{"location":"code/interfaces/TableOperations.TableOperations/#interface-tableoperations","text":"TableOperations .TableOperations Offer CRUD-style operations on a table.","title":"Interface: TableOperations"},{"location":"code/interfaces/TableOperations.TableOperations/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/TableOperations.TableOperations/#methods","text":"create destroy getTableId update upsert","title":"Methods"},{"location":"code/interfaces/TableOperations.TableOperations/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/TableOperations.TableOperations/#create","text":"\u25b8 create ( records , options? ): Promise < MinimalRecord > Create a record or records.","title":"create"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters","text":"Name Type records NewRecord options? OpOptions","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns","text":"Promise < MinimalRecord >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#destroy","text":"\u25b8 destroy ( recordIds ): Promise < void > Delete a record or records.","title":"destroy"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters_1","text":"Name Type recordIds number | number []","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_1","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#gettableid","text":"\u25b8 getTableId (): Promise < string > Determine the tableId of the table.","title":"getTableId"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_2","text":"Promise < string >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#update","text":"\u25b8 update ( records , options? ): Promise < void > Update a record or records.","title":"update"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters_2","text":"Name Type records Record | Record [] options? OpOptions","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_3","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/TableOperations.TableOperations/#upsert","text":"\u25b8 upsert ( records , options? ): Promise < void > Add or update a record or records.","title":"upsert"},{"location":"code/interfaces/TableOperations.TableOperations/#parameters_3","text":"Name Type records AddOrUpdateRecord | AddOrUpdateRecord [] options? UpsertOptions","title":"Parameters"},{"location":"code/interfaces/TableOperations.TableOperations/#returns_4","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/TableOperations.UpsertOptions/","text":"Interface: UpsertOptions # TableOperations .UpsertOptions Extra options for upserts. Hierarchy # OpOptions \u21b3 UpsertOptions Table of contents # Properties # add allowEmptyRequire onMany parseStrings update Properties # add # \u2022 Optional add : boolean Permit inserting a record. Defaults to true. allowEmptyRequire # \u2022 Optional allowEmptyRequire : boolean Allow \u201cwildcard\u201d operation. Defaults to false. onMany # \u2022 Optional onMany : \"all\" | \"none\" | \"first\" Whether to update none, one, or all matching records. Defaults to \u201cfirst\u201d. parseStrings # \u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true. Inherited from # OpOptions . parseStrings update # \u2022 Optional update : boolean Permit updating a record. Defaults to true.","title":"Interface: UpsertOptions"},{"location":"code/interfaces/TableOperations.UpsertOptions/#interface-upsertoptions","text":"TableOperations .UpsertOptions Extra options for upserts.","title":"Interface: UpsertOptions"},{"location":"code/interfaces/TableOperations.UpsertOptions/#hierarchy","text":"OpOptions \u21b3 UpsertOptions","title":"Hierarchy"},{"location":"code/interfaces/TableOperations.UpsertOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/TableOperations.UpsertOptions/#properties","text":"add allowEmptyRequire onMany parseStrings update","title":"Properties"},{"location":"code/interfaces/TableOperations.UpsertOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/TableOperations.UpsertOptions/#add","text":"\u2022 Optional add : boolean Permit inserting a record. Defaults to true.","title":"add"},{"location":"code/interfaces/TableOperations.UpsertOptions/#allowemptyrequire","text":"\u2022 Optional allowEmptyRequire : boolean Allow \u201cwildcard\u201d operation. Defaults to false.","title":"allowEmptyRequire"},{"location":"code/interfaces/TableOperations.UpsertOptions/#onmany","text":"\u2022 Optional onMany : \"all\" | \"none\" | \"first\" Whether to update none, one, or all matching records. Defaults to \u201cfirst\u201d.","title":"onMany"},{"location":"code/interfaces/TableOperations.UpsertOptions/#parsestrings","text":"\u2022 Optional parseStrings : boolean Whether to parse strings based on the column type. Defaults to true.","title":"parseStrings"},{"location":"code/interfaces/TableOperations.UpsertOptions/#inherited-from","text":"OpOptions . parseStrings","title":"Inherited from"},{"location":"code/interfaces/TableOperations.UpsertOptions/#update","text":"\u2022 Optional update : boolean Permit updating a record. Defaults to true.","title":"update"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/","text":"Interface: AccessTokenOptions # grist-plugin-api .AccessTokenOptions Options when creating access tokens. Table of contents # Properties # readOnly Properties # readOnly # \u2022 Optional readOnly : boolean Restrict use of token to reading only","title":"Interface: AccessTokenOptions"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#interface-accesstokenoptions","text":"grist-plugin-api .AccessTokenOptions Options when creating access tokens.","title":"Interface: AccessTokenOptions"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#properties","text":"readOnly","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenOptions/#readonly","text":"\u2022 Optional readOnly : boolean Restrict use of token to reading only","title":"readOnly"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/","text":"Interface: AccessTokenResult # grist-plugin-api .AccessTokenResult Access token information, including the token string itself, a base URL for API calls for which the access token can be used, and the time-to-live the token was created with. Table of contents # Properties # baseUrl token ttlMsecs Properties # baseUrl # \u2022 baseUrl : string The base url of the API for which the token can be used. Currently tokens are associated with a single document, so the base url will be something like https://..../api/docs/DOCID Access tokens currently only grant access to endpoints dealing with the internal content of a document (such as tables and cells) and not its metadata (such as the document name or who it is shared with). token # \u2022 token : string The token string, which can currently be provided in an api call as a query parameter called \u201cauth\u201d ttlMsecs # \u2022 ttlMsecs : number Number of milliseconds the access token will remain valid for after creation. This will be several minutes.","title":"Interface: AccessTokenResult"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#interface-accesstokenresult","text":"grist-plugin-api .AccessTokenResult Access token information, including the token string itself, a base URL for API calls for which the access token can be used, and the time-to-live the token was created with.","title":"Interface: AccessTokenResult"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#properties","text":"baseUrl token ttlMsecs","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#baseurl","text":"\u2022 baseUrl : string The base url of the API for which the token can be used. Currently tokens are associated with a single document, so the base url will be something like https://..../api/docs/DOCID Access tokens currently only grant access to endpoints dealing with the internal content of a document (such as tables and cells) and not its metadata (such as the document name or who it is shared with).","title":"baseUrl"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#token","text":"\u2022 token : string The token string, which can currently be provided in an api call as a query parameter called \u201cauth\u201d","title":"token"},{"location":"code/interfaces/grist_plugin_api.AccessTokenResult/#ttlmsecs","text":"\u2022 ttlMsecs : number Number of milliseconds the access token will remain valid for after creation. This will be several minutes.","title":"ttlMsecs"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/","text":"Interface: ColumnToMap # grist-plugin-api .ColumnToMap API definitions for CustomSection plugins. Table of contents # Properties # allowMultiple description name optional strictType title type Properties # allowMultiple # \u2022 Optional allowMultiple : boolean Allow multiple column assignment, the result will be list of mapped table column names. description # \u2022 Optional description : null | string Optional long description of a column (used as a help text in section mapping). name # \u2022 name : string Column name that Widget expects. Must be a valid JSON property name. optional # \u2022 Optional optional : boolean Mark column as optional all columns are required by default. strictType # \u2022 Optional strictType : boolean Match column type strictly, so \u201cAny\u201d will require \u201cAny\u201d and not any other type. title # \u2022 Optional title : null | string Title or short description of a column (used as a label in section mapping). type # \u2022 Optional type : string Column types (as comma separated list), by default \u201cAny\u201d, what means that any type is allowed (unless strictType is true).","title":"Interface: ColumnToMap"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#interface-columntomap","text":"grist-plugin-api .ColumnToMap API definitions for CustomSection plugins.","title":"Interface: ColumnToMap"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#properties","text":"allowMultiple description name optional strictType title type","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#allowmultiple","text":"\u2022 Optional allowMultiple : boolean Allow multiple column assignment, the result will be list of mapped table column names.","title":"allowMultiple"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#description","text":"\u2022 Optional description : null | string Optional long description of a column (used as a help text in section mapping).","title":"description"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#name","text":"\u2022 name : string Column name that Widget expects. Must be a valid JSON property name.","title":"name"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#optional","text":"\u2022 Optional optional : boolean Mark column as optional all columns are required by default.","title":"optional"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#stricttype","text":"\u2022 Optional strictType : boolean Match column type strictly, so \u201cAny\u201d will require \u201cAny\u201d and not any other type.","title":"strictType"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#title","text":"\u2022 Optional title : null | string Title or short description of a column (used as a label in section mapping).","title":"title"},{"location":"code/interfaces/grist_plugin_api.ColumnToMap/#type","text":"\u2022 Optional type : string Column types (as comma separated list), by default \u201cAny\u201d, what means that any type is allowed (unless strictType is true).","title":"type"},{"location":"code/interfaces/grist_plugin_api.CursorPos/","text":"Interface: CursorPos # grist-plugin-api .CursorPos Represents the position of an active cursor on a page. Table of contents # Properties # fieldIndex linkingRowIds rowId rowIndex sectionId Properties # fieldIndex # \u2022 Optional fieldIndex : number The index of the selected field in the current view. linkingRowIds # \u2022 Optional linkingRowIds : UIRowId [] When in a linked section, CursorPos may include which rows in the controlling sections are selected: the rowId in the linking-source section, in that section\u2019s linking source, etc. rowId # \u2022 Optional rowId : UIRowId The rowId (value of the id column) of the current cursor position, or \u2018new\u2019 if the cursor is on a new row. rowIndex # \u2022 Optional rowIndex : number The index of the current row in the current view. sectionId # \u2022 Optional sectionId : number The id of a section that this cursor is in. Ignored when setting a cursor position for a particular view.","title":"Interface: CursorPos"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#interface-cursorpos","text":"grist-plugin-api .CursorPos Represents the position of an active cursor on a page.","title":"Interface: CursorPos"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#properties","text":"fieldIndex linkingRowIds rowId rowIndex sectionId","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#fieldindex","text":"\u2022 Optional fieldIndex : number The index of the selected field in the current view.","title":"fieldIndex"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#linkingrowids","text":"\u2022 Optional linkingRowIds : UIRowId [] When in a linked section, CursorPos may include which rows in the controlling sections are selected: the rowId in the linking-source section, in that section\u2019s linking source, etc.","title":"linkingRowIds"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#rowid","text":"\u2022 Optional rowId : UIRowId The rowId (value of the id column) of the current cursor position, or \u2018new\u2019 if the cursor is on a new row.","title":"rowId"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#rowindex","text":"\u2022 Optional rowIndex : number The index of the current row in the current view.","title":"rowIndex"},{"location":"code/interfaces/grist_plugin_api.CursorPos/#sectionid","text":"\u2022 Optional sectionId : number The id of a section that this cursor is in. Ignored when setting a cursor position for a particular view.","title":"sectionId"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/","text":"Interface: CustomSectionAPI # grist-plugin-api .CustomSectionAPI Interface for the mapping of a custom widget. Table of contents # Methods # configure mappings Methods # configure # \u25b8 configure ( customOptions ): Promise < void > Initial request from a Custom Widget that wants to declare its requirements. Parameters # Name Type customOptions InteractionOptionsRequest Returns # Promise < void > mappings # \u25b8 mappings (): Promise < null | WidgetColumnMap > Returns current widget configuration (if requested through configuration method). Returns # Promise < null | WidgetColumnMap >","title":"Interface: CustomSectionAPI"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#interface-customsectionapi","text":"grist-plugin-api .CustomSectionAPI Interface for the mapping of a custom widget.","title":"Interface: CustomSectionAPI"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#methods","text":"configure mappings","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#configure","text":"\u25b8 configure ( customOptions ): Promise < void > Initial request from a Custom Widget that wants to declare its requirements.","title":"configure"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#parameters","text":"Name Type customOptions InteractionOptionsRequest","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#mappings","text":"\u25b8 mappings (): Promise < null | WidgetColumnMap > Returns current widget configuration (if requested through configuration method).","title":"mappings"},{"location":"code/interfaces/grist_plugin_api.CustomSectionAPI/#returns_1","text":"Promise < null | WidgetColumnMap >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/","text":"Interface: FetchSelectedOptions # grist-plugin-api .FetchSelectedOptions Options for functions which fetch data from the selected table or record: onRecords onRecord fetchSelectedRecord fetchSelectedTable GristView.fetchSelectedRecord GristView.fetchSelectedTable The different methods have different default values for keepEncoded and format . Table of contents # Properties # format includeColumns keepEncoded Properties # format # \u2022 Optional format : \"columns\" | \"rows\" rows , the returned data will be an array of objects, one per row, with column names as keys. columns , the returned data will be an object with column names as keys, and arrays of values. includeColumns # \u2022 Optional includeColumns : \"shown\" | \"normal\" | \"all\" shown (default): return only columns that are explicitly shown in the right panel configuration of the widget. This is the only value that doesn\u2019t require full access. normal : return all \u2018normal\u2019 columns, regardless of whether the user has shown them. all : also return special invisible columns like manualSort and display helper columns. keepEncoded # \u2022 Optional keepEncoded : boolean true : the returned data will contain raw CellValue \u2018s. false : the values will be decoded, replacing e.g. ['D', timestamp] with a moment date.","title":"Interface: FetchSelectedOptions"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#interface-fetchselectedoptions","text":"grist-plugin-api .FetchSelectedOptions Options for functions which fetch data from the selected table or record: onRecords onRecord fetchSelectedRecord fetchSelectedTable GristView.fetchSelectedRecord GristView.fetchSelectedTable The different methods have different default values for keepEncoded and format .","title":"Interface: FetchSelectedOptions"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#properties","text":"format includeColumns keepEncoded","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#format","text":"\u2022 Optional format : \"columns\" | \"rows\" rows , the returned data will be an array of objects, one per row, with column names as keys. columns , the returned data will be an object with column names as keys, and arrays of values.","title":"format"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#includecolumns","text":"\u2022 Optional includeColumns : \"shown\" | \"normal\" | \"all\" shown (default): return only columns that are explicitly shown in the right panel configuration of the widget. This is the only value that doesn\u2019t require full access. normal : return all \u2018normal\u2019 columns, regardless of whether the user has shown them. all : also return special invisible columns like manualSort and display helper columns.","title":"includeColumns"},{"location":"code/interfaces/grist_plugin_api.FetchSelectedOptions/#keepencoded","text":"\u2022 Optional keepEncoded : boolean true : the returned data will contain raw CellValue \u2018s. false : the values will be decoded, replacing e.g. ['D', timestamp] with a moment date.","title":"keepEncoded"},{"location":"code/interfaces/grist_plugin_api.GristColumn/","text":"Interface: GristColumn # grist-plugin-api .GristColumn Metadata about a single column.","title":"Interface: GristColumn"},{"location":"code/interfaces/grist_plugin_api.GristColumn/#interface-gristcolumn","text":"grist-plugin-api .GristColumn Metadata about a single column.","title":"Interface: GristColumn"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/","text":"Interface: GristDocAPI # grist-plugin-api .GristDocAPI Allows getting information from and interacting with the Grist document to which a plugin or widget is attached. Table of contents # Methods # applyUserActions fetchTable getAccessToken getDocName listTables Methods # applyUserActions # \u25b8 applyUserActions ( actions , options? ): Promise < any > Applies an array of user actions. Parameters # Name Type actions any [][] options? any Returns # Promise < any > fetchTable # \u25b8 fetchTable ( tableId ): Promise < any > Returns a complete table of data as GristData.RowRecords , including the \u2018id\u2019 column. Do not modify the returned arrays in-place, especially if used directly (not over RPC). Parameters # Name Type tableId string Returns # Promise < any > getAccessToken # \u25b8 getAccessToken ( options ): Promise < AccessTokenResult > Get a token for out-of-band access to the document. Parameters # Name Type options AccessTokenOptions Returns # Promise < AccessTokenResult > getDocName # \u25b8 getDocName (): Promise < string > Returns an identifier for the document. Returns # Promise < string > listTables # \u25b8 listTables (): Promise < string []> Returns a sorted list of table IDs. Returns # Promise < string []>","title":"Interface: GristDocAPI"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#interface-gristdocapi","text":"grist-plugin-api .GristDocAPI Allows getting information from and interacting with the Grist document to which a plugin or widget is attached.","title":"Interface: GristDocAPI"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#methods","text":"applyUserActions fetchTable getAccessToken getDocName listTables","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#applyuseractions","text":"\u25b8 applyUserActions ( actions , options? ): Promise < any > Applies an array of user actions.","title":"applyUserActions"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#parameters","text":"Name Type actions any [][] options? any","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#fetchtable","text":"\u25b8 fetchTable ( tableId ): Promise < any > Returns a complete table of data as GristData.RowRecords , including the \u2018id\u2019 column. Do not modify the returned arrays in-place, especially if used directly (not over RPC).","title":"fetchTable"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#parameters_1","text":"Name Type tableId string","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_1","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#getaccesstoken","text":"\u25b8 getAccessToken ( options ): Promise < AccessTokenResult > Get a token for out-of-band access to the document.","title":"getAccessToken"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#parameters_2","text":"Name Type options AccessTokenOptions","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_2","text":"Promise < AccessTokenResult >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#getdocname","text":"\u25b8 getDocName (): Promise < string > Returns an identifier for the document.","title":"getDocName"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_3","text":"Promise < string >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#listtables","text":"\u25b8 listTables (): Promise < string []> Returns a sorted list of table IDs.","title":"listTables"},{"location":"code/interfaces/grist_plugin_api.GristDocAPI/#returns_4","text":"Promise < string []>","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristTable/","text":"Interface: GristTable # grist-plugin-api .GristTable Metadata and data for a table.","title":"Interface: GristTable"},{"location":"code/interfaces/grist_plugin_api.GristTable/#interface-gristtable","text":"grist-plugin-api .GristTable Metadata and data for a table.","title":"Interface: GristTable"},{"location":"code/interfaces/grist_plugin_api.GristView/","text":"Interface: GristView # grist-plugin-api .GristView Interface for the data backing a single widget. Table of contents # Methods # allowSelectBy fetchSelectedRecord fetchSelectedTable setCursorPos setSelectedRows Methods # allowSelectBy # \u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message. Returns # Promise < void > fetchSelectedRecord # \u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Fetches selected record by its rowId . By default, options.keepEncoded is true . Parameters # Name Type rowId number options? FetchSelectedOptions Returns # Promise < any > fetchSelectedTable # \u25b8 fetchSelectedTable ( options? ): Promise < any > Like GristDocAPI.fetchTable , but gets data for the custom section specifically, if there is any. By default, options.keepEncoded is true and format is columns . Parameters # Name Type options? FetchSelectedOptions Returns # Promise < any > setCursorPos # \u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking. Parameters # Name Type pos CursorPos Returns # Promise < void > setSelectedRows # \u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget. Parameters # Name Type rowIds null | number [] Returns # Promise < void >","title":"Interface: GristView"},{"location":"code/interfaces/grist_plugin_api.GristView/#interface-gristview","text":"grist-plugin-api .GristView Interface for the data backing a single widget.","title":"Interface: GristView"},{"location":"code/interfaces/grist_plugin_api.GristView/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.GristView/#methods","text":"allowSelectBy fetchSelectedRecord fetchSelectedTable setCursorPos setSelectedRows","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristView/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.GristView/#allowselectby","text":"\u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message.","title":"allowSelectBy"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#fetchselectedrecord","text":"\u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Fetches selected record by its rowId . By default, options.keepEncoded is true .","title":"fetchSelectedRecord"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters","text":"Name Type rowId number options? FetchSelectedOptions","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_1","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#fetchselectedtable","text":"\u25b8 fetchSelectedTable ( options? ): Promise < any > Like GristDocAPI.fetchTable , but gets data for the custom section specifically, if there is any. By default, options.keepEncoded is true and format is columns .","title":"fetchSelectedTable"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters_1","text":"Name Type options? FetchSelectedOptions","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_2","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#setcursorpos","text":"\u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking.","title":"setCursorPos"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters_2","text":"Name Type pos CursorPos","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_3","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.GristView/#setselectedrows","text":"\u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget.","title":"setSelectedRows"},{"location":"code/interfaces/grist_plugin_api.GristView/#parameters_3","text":"Name Type rowIds null | number []","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.GristView/#returns_4","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/","text":"Interface: InteractionOptions # grist-plugin-api .InteractionOptions Widget configuration set and approved by Grist, sent as part of ready message. Table of contents # Properties # accessLevel Properties # accessLevel # \u2022 accessLevel : string Granted access level.","title":"Interface: InteractionOptions"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#interface-interactionoptions","text":"grist-plugin-api .InteractionOptions Widget configuration set and approved by Grist, sent as part of ready message.","title":"Interface: InteractionOptions"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#properties","text":"accessLevel","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptions/#accesslevel","text":"\u2022 accessLevel : string Granted access level.","title":"accessLevel"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/","text":"Interface: InteractionOptionsRequest # grist-plugin-api .InteractionOptionsRequest Initial message sent by the CustomWidget with initial requirements. Table of contents # Properties # allowSelectBy columns hasCustomOptions requiredAccess Properties # allowSelectBy # \u2022 Optional allowSelectBy : boolean Show widget as linking source. columns # \u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget. hasCustomOptions # \u2022 Optional hasCustomOptions : boolean Instructs Grist to show additional menu options that will trigger onEditOptions callback, that Widget can use to show custom options screen. requiredAccess # \u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level.","title":"Interface: InteractionOptionsRequest"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#interface-interactionoptionsrequest","text":"grist-plugin-api .InteractionOptionsRequest Initial message sent by the CustomWidget with initial requirements.","title":"Interface: InteractionOptionsRequest"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#properties","text":"allowSelectBy columns hasCustomOptions requiredAccess","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#allowselectby","text":"\u2022 Optional allowSelectBy : boolean Show widget as linking source.","title":"allowSelectBy"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#columns","text":"\u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget.","title":"columns"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#hascustomoptions","text":"\u2022 Optional hasCustomOptions : boolean Instructs Grist to show additional menu options that will trigger onEditOptions callback, that Widget can use to show custom options screen.","title":"hasCustomOptions"},{"location":"code/interfaces/grist_plugin_api.InteractionOptionsRequest/#requiredaccess","text":"\u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level.","title":"requiredAccess"},{"location":"code/interfaces/grist_plugin_api.ParseOptionSchema/","text":"Interface: ParseOptionSchema # grist-plugin-api .ParseOptionSchema ParseOptionSchema contains information for generaing parse options UI","title":"Interface: ParseOptionSchema"},{"location":"code/interfaces/grist_plugin_api.ParseOptionSchema/#interface-parseoptionschema","text":"grist-plugin-api .ParseOptionSchema ParseOptionSchema contains information for generaing parse options UI","title":"Interface: ParseOptionSchema"},{"location":"code/interfaces/grist_plugin_api.ParseOptions/","text":"Interface: ParseOptions # grist-plugin-api .ParseOptions ParseOptions contains parse options depending on plugin, number of rows, which is special option that can be used for any plugin and schema for generating parse options UI","title":"Interface: ParseOptions"},{"location":"code/interfaces/grist_plugin_api.ParseOptions/#interface-parseoptions","text":"grist-plugin-api .ParseOptions ParseOptions contains parse options depending on plugin, number of rows, which is special option that can be used for any plugin and schema for generating parse options UI","title":"Interface: ParseOptions"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/","text":"Interface: ReadyPayload # grist-plugin-api .ReadyPayload Options when initializing connection to Grist. Hierarchy # Omit < InteractionOptionsRequest , \"hasCustomOptions\" > \u21b3 ReadyPayload Table of contents # Properties # allowSelectBy columns onEditOptions requiredAccess Properties # allowSelectBy # \u2022 Optional allowSelectBy : boolean Show widget as linking source. Inherited from # Omit.allowSelectBy columns # \u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget. Inherited from # Omit.columns onEditOptions # \u2022 Optional onEditOptions : () => unknown Type declaration # \u25b8 (): unknown Handler that will be called by Grist to open additional configuration panel inside the Custom Widget. Returns # unknown requiredAccess # \u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level. Inherited from # Omit.requiredAccess","title":"Interface: ReadyPayload"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#interface-readypayload","text":"grist-plugin-api .ReadyPayload Options when initializing connection to Grist.","title":"Interface: ReadyPayload"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#hierarchy","text":"Omit < InteractionOptionsRequest , \"hasCustomOptions\" > \u21b3 ReadyPayload","title":"Hierarchy"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#properties","text":"allowSelectBy columns onEditOptions requiredAccess","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#properties_1","text":"","title":"Properties"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#allowselectby","text":"\u2022 Optional allowSelectBy : boolean Show widget as linking source.","title":"allowSelectBy"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#inherited-from","text":"Omit.allowSelectBy","title":"Inherited from"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#columns","text":"\u2022 Optional columns : ColumnsToMap Tells Grist what columns Custom Widget expects and allows user to map between existing column names and those requested by Custom Widget.","title":"columns"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#inherited-from_1","text":"Omit.columns","title":"Inherited from"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#oneditoptions","text":"\u2022 Optional onEditOptions : () => unknown","title":"onEditOptions"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#type-declaration","text":"\u25b8 (): unknown Handler that will be called by Grist to open additional configuration panel inside the Custom Widget.","title":"Type declaration"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#returns","text":"unknown","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#requiredaccess","text":"\u2022 Optional requiredAccess : string Required access level. If it wasn\u2019t granted already, Grist will prompt user to change the current access level.","title":"requiredAccess"},{"location":"code/interfaces/grist_plugin_api.ReadyPayload/#inherited-from_2","text":"Omit.requiredAccess","title":"Inherited from"},{"location":"code/interfaces/grist_plugin_api.RenderOptions/","text":"Interface: RenderOptions # grist-plugin-api .RenderOptions Options for the grist.render function.","title":"Interface: RenderOptions"},{"location":"code/interfaces/grist_plugin_api.RenderOptions/#interface-renderoptions","text":"grist-plugin-api .RenderOptions Options for the grist.render function.","title":"Interface: RenderOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/","text":"Interface: WidgetAPI # grist-plugin-api .WidgetAPI API to manage Custom Widget state. Table of contents # Methods # clearOptions getOption getOptions setOption setOptions Methods # clearOptions # \u25b8 clearOptions (): Promise < void > Clears all the options. Returns # Promise < void > getOption # \u25b8 getOption ( key ): Promise < any > Get single value from Widget options object. Parameters # Name Type key string Returns # Promise < any > getOptions # \u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object. Returns # Promise < null | object > setOption # \u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary). Parameters # Name Type key string value any Returns # Promise < void > setOptions # \u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget. Parameters # Name Type options Object Returns # Promise < void >","title":"Interface: WidgetAPI"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#interface-widgetapi","text":"grist-plugin-api .WidgetAPI API to manage Custom Widget state.","title":"Interface: WidgetAPI"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#methods","text":"clearOptions getOption getOptions setOption setOptions","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#methods_1","text":"","title":"Methods"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#clearoptions","text":"\u25b8 clearOptions (): Promise < void > Clears all the options.","title":"clearOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#getoption","text":"\u25b8 getOption ( key ): Promise < any > Get single value from Widget options object.","title":"getOption"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#parameters","text":"Name Type key string","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_1","text":"Promise < any >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#getoptions","text":"\u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object.","title":"getOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_2","text":"Promise < null | object >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#setoption","text":"\u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary).","title":"setOption"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#parameters_1","text":"Name Type key string value any","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_3","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#setoptions","text":"\u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget.","title":"setOptions"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#parameters_2","text":"Name Type options Object","title":"Parameters"},{"location":"code/interfaces/grist_plugin_api.WidgetAPI/#returns_4","text":"Promise < void >","title":"Returns"},{"location":"code/interfaces/grist_plugin_api.WidgetColumnMap/","text":"Interface: WidgetColumnMap # grist-plugin-api .WidgetColumnMap Current columns mapping between viewFields in section and Custom widget.","title":"Interface: WidgetColumnMap"},{"location":"code/interfaces/grist_plugin_api.WidgetColumnMap/#interface-widgetcolumnmap","text":"grist-plugin-api .WidgetColumnMap Current columns mapping between viewFields in section and Custom widget.","title":"Interface: WidgetColumnMap"},{"location":"code/modules/DocApiTypes/","text":"Module: DocApiTypes # Table of contents # Interfaces # AddOrUpdateRecord MinimalRecord NewRecord Record RecordsPatch RecordsPost RecordsPut SqlPost TablePost","title":"Module: DocApiTypes"},{"location":"code/modules/DocApiTypes/#module-docapitypes","text":"","title":"Module: DocApiTypes"},{"location":"code/modules/DocApiTypes/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/DocApiTypes/#interfaces","text":"AddOrUpdateRecord MinimalRecord NewRecord Record RecordsPatch RecordsPost RecordsPut SqlPost TablePost","title":"Interfaces"},{"location":"code/modules/GristData/","text":"Module: GristData # Table of contents # Enumerations # GristObjCode Interfaces # RowRecord RowRecords Type Aliases # CellValue Type Aliases # CellValue # \u01ac CellValue : number | string | boolean | null | [ GristObjCode , \u2026unknown[]] Possible types of cell content. Each CellValue may either be a primitive (e.g. true , 123 , \"hello\" , null ) or a tuple (JavaScript Array) representing a Grist object. The first element of the tuple is a string character representing the object code. For example, [\"L\", \"foo\", \"bar\"] is a CellValue of a Choice List column, where \"L\" is the type, and \"foo\" and \"bar\" are the choices. Grist Object Types # Code Type L List, e.g. [\"L\", \"foo\", \"bar\"] or [\"L\", 1, 2] l LookUp, as [\"l\", value, options] O Dict, as [\"O\", {key: value, ...}] D DateTimes, as [\"D\", timestamp, timezone] , e.g. [\"D\", 1704945919, \"UTC\"] d Date, as [\"d\", timestamp] , e.g. [\"d\", 1704844800] C Censored, as [\"C\"] R Reference, as [\"R\", table_id, row_id] , e.g. [\"R\", \"People\", 17] r ReferenceList, as [\"r\", table_id, row_id_list] , e.g. [\"r\", \"People\", [1,2]] E Exception, as [\"E\", name, ...] , e.g. [\"E\", \"ValueError\"] P Pending, as [\"P\"] U Unmarshallable, as [\"U\", text_representation] V Version, as [\"V\", version_obj]","title":"Module: GristData"},{"location":"code/modules/GristData/#module-gristdata","text":"","title":"Module: GristData"},{"location":"code/modules/GristData/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/GristData/#enumerations","text":"GristObjCode","title":"Enumerations"},{"location":"code/modules/GristData/#interfaces","text":"RowRecord RowRecords","title":"Interfaces"},{"location":"code/modules/GristData/#type-aliases","text":"CellValue","title":"Type Aliases"},{"location":"code/modules/GristData/#type-aliases_1","text":"","title":"Type Aliases"},{"location":"code/modules/GristData/#cellvalue","text":"\u01ac CellValue : number | string | boolean | null | [ GristObjCode , \u2026unknown[]] Possible types of cell content. Each CellValue may either be a primitive (e.g. true , 123 , \"hello\" , null ) or a tuple (JavaScript Array) representing a Grist object. The first element of the tuple is a string character representing the object code. For example, [\"L\", \"foo\", \"bar\"] is a CellValue of a Choice List column, where \"L\" is the type, and \"foo\" and \"bar\" are the choices.","title":"CellValue"},{"location":"code/modules/GristData/#grist-object-types","text":"Code Type L List, e.g. [\"L\", \"foo\", \"bar\"] or [\"L\", 1, 2] l LookUp, as [\"l\", value, options] O Dict, as [\"O\", {key: value, ...}] D DateTimes, as [\"D\", timestamp, timezone] , e.g. [\"D\", 1704945919, \"UTC\"] d Date, as [\"d\", timestamp] , e.g. [\"d\", 1704844800] C Censored, as [\"C\"] R Reference, as [\"R\", table_id, row_id] , e.g. [\"R\", \"People\", 17] r ReferenceList, as [\"r\", table_id, row_id_list] , e.g. [\"r\", \"People\", [1,2]] E Exception, as [\"E\", name, ...] , e.g. [\"E\", \"ValueError\"] P Pending, as [\"P\"] U Unmarshallable, as [\"U\", text_representation] V Version, as [\"V\", version_obj]","title":"Grist Object Types"},{"location":"code/modules/TableOperations/","text":"Module: TableOperations # Table of contents # Interfaces # OpOptions TableOperations UpsertOptions","title":"Module: TableOperations"},{"location":"code/modules/TableOperations/#module-tableoperations","text":"","title":"Module: TableOperations"},{"location":"code/modules/TableOperations/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/TableOperations/#interfaces","text":"OpOptions TableOperations UpsertOptions","title":"Interfaces"},{"location":"code/modules/grist_plugin_api/","text":"Module: grist-plugin-api # Table of contents # Interfaces # AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap Type Aliases # ColumnsToMap UIRowId Variables # checkers docApi sectionApi selectedTable viewApi widgetApi Functions # allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows Type Aliases # ColumnsToMap # \u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget. UIRowId # \u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row. Variables # checkers # \u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above. docApi # \u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default. sectionApi # \u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget. selectedTable # \u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets). viewApi # \u2022 Const viewApi : GristView Interface for the records backing a custom widget. widgetApi # \u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget. Functions # allowSelectBy # \u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message. Returns # Promise < void > clearOptions # \u25b8 clearOptions (): Promise < void > Clears all the options. Returns # Promise < void > fetchSelectedRecord # \u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default. Parameters # Name Type rowId number options FetchSelectedOptions Returns # Promise < any > fetchSelectedTable # \u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default. Parameters # Name Type options FetchSelectedOptions Returns # Promise < any > getAccessToken # \u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); }); Parameters # Name Type options? AccessTokenOptions Returns # Promise < AccessTokenResult > getOption # \u25b8 getOption ( key ): Promise < any > Get single value from Widget options object. Parameters # Name Type key string Returns # Promise < any > getOptions # \u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object. Returns # Promise < null | object > getTable # \u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted. Parameters # Name Type tableId? string Returns # TableOperations mapColumnNames # \u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean Returns # any mapColumnNamesBack # \u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically. Parameters # Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap Returns # any on # \u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101 Parameters # Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function Returns # Rpc onNewRecord # \u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected. Parameters # Name Type callback ( mappings : null | WidgetColumnMap ) => unknown Returns # void onOptions # \u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it. Parameters # Name Type callback ( options : any , settings : InteractionOptions ) => unknown Returns # void onRecord # \u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false . Parameters # Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void onRecords # \u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false . Parameters # Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions Returns # void ready # \u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called. Parameters # Name Type settings? ReadyPayload Returns # void setCursorPos # \u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking. Parameters # Name Type pos CursorPos Returns # Promise < void > setOption # \u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary). Parameters # Name Type key string value any Returns # Promise < void > setOptions # \u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget. Parameters # Name Type options Object Returns # Promise < void > setSelectedRows # \u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget. Parameters # Name Type rowIds null | number [] Returns # Promise < void >","title":"grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#module-grist-plugin-api","text":"","title":"Module: grist-plugin-api"},{"location":"code/modules/grist_plugin_api/#table-of-contents","text":"","title":"Table of contents"},{"location":"code/modules/grist_plugin_api/#interfaces","text":"AccessTokenOptions AccessTokenResult ColumnToMap CursorPos CustomSectionAPI FetchSelectedOptions GristColumn GristDocAPI GristTable GristView InteractionOptions InteractionOptionsRequest ParseOptionSchema ParseOptions ReadyPayload RenderOptions WidgetAPI WidgetColumnMap","title":"Interfaces"},{"location":"code/modules/grist_plugin_api/#type-aliases","text":"ColumnsToMap UIRowId","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#variables","text":"checkers docApi sectionApi selectedTable viewApi widgetApi","title":"Variables"},{"location":"code/modules/grist_plugin_api/#functions","text":"allowSelectBy clearOptions fetchSelectedRecord fetchSelectedTable getAccessToken getOption getOptions getTable mapColumnNames mapColumnNamesBack on onNewRecord onOptions onRecord onRecords ready setCursorPos setOption setOptions setSelectedRows","title":"Functions"},{"location":"code/modules/grist_plugin_api/#type-aliases_1","text":"","title":"Type Aliases"},{"location":"code/modules/grist_plugin_api/#columnstomap","text":"\u01ac ColumnsToMap : ( string | ColumnToMap )[] Tells Grist what columns a Custom Widget expects and allows users to map between existing column names and those requested by the Custom Widget.","title":"ColumnsToMap"},{"location":"code/modules/grist_plugin_api/#uirowid","text":"\u01ac UIRowId : number | \"new\" Represents the id of a row in a table. The value of the id column. Might be a number or \u2018new\u2019 value for a new row.","title":"UIRowId"},{"location":"code/modules/grist_plugin_api/#variables_1","text":"","title":"Variables"},{"location":"code/modules/grist_plugin_api/#checkers","text":"\u2022 Const checkers : Pick < ICheckerSuite , \"CustomSectionAPI\" | \"ParseOptions\" | \"ParseFileResult\" | \"FileSource\" | \"ParseOptionSchema\" | \"GristTables\" | \"EditOptionsAPI\" | \"ParseFileAPI\" | \"RenderTarget\" | \"RenderOptions\" | \"ComponentKind\" | \"GristAPI\" | \"GristDocAPI\" | \"GristView\" | \"GristColumn\" | \"GristTable\" | \"ImportSourceAPI\" | \"ImportProcessorAPI\" | \"ImportSource\" | \"FileContent\" | \"FileListItem\" | \"URL\" | \"InternalImportSourceAPI\" | \"Storage\" | \"WidgetAPI\" > We also create and export a global checker object that includes all of the types above.","title":"checkers"},{"location":"code/modules/grist_plugin_api/#docapi","text":"\u2022 Const docApi : GristDocAPI & GristView A collection of methods for fetching document data. The fetchSelectedTable and fetchSelectedRecord methods are overridden to decode data by default.","title":"docApi"},{"location":"code/modules/grist_plugin_api/#sectionapi","text":"\u2022 Const sectionApi : CustomSectionAPI Interface for the mapping of a custom widget.","title":"sectionApi"},{"location":"code/modules/grist_plugin_api/#selectedtable","text":"\u2022 Const selectedTable : TableOperations Get the current selected table (for custom widgets).","title":"selectedTable"},{"location":"code/modules/grist_plugin_api/#viewapi","text":"\u2022 Const viewApi : GristView Interface for the records backing a custom widget.","title":"viewApi"},{"location":"code/modules/grist_plugin_api/#widgetapi","text":"\u2022 Const widgetApi : WidgetAPI Interface for the state of a custom widget.","title":"widgetApi"},{"location":"code/modules/grist_plugin_api/#functions_1","text":"","title":"Functions"},{"location":"code/modules/grist_plugin_api/#allowselectby","text":"\u25b8 allowSelectBy (): Promise < void > Deprecated now. It was used for filtering selected table by setSelectedRows method. Now the preferred way it to use ready message.","title":"allowSelectBy"},{"location":"code/modules/grist_plugin_api/#returns","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#clearoptions","text":"\u25b8 clearOptions (): Promise < void > Clears all the options.","title":"clearOptions"},{"location":"code/modules/grist_plugin_api/#returns_1","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedrecord","text":"\u25b8 fetchSelectedRecord ( rowId , options? ): Promise < any > Same as GristView.fetchSelectedRecord , but the option keepEncoded is false by default.","title":"fetchSelectedRecord"},{"location":"code/modules/grist_plugin_api/#parameters","text":"Name Type rowId number options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_2","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#fetchselectedtable","text":"\u25b8 fetchSelectedTable ( options? ): Promise < any > Same as GristView.fetchSelectedTable , but the option keepEncoded is false by default.","title":"fetchSelectedTable"},{"location":"code/modules/grist_plugin_api/#parameters_1","text":"Name Type options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_3","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getaccesstoken","text":"\u25b8 getAccessToken ( options? ): Promise < AccessTokenResult > Get an access token, for making API calls outside of the custom widget API. There is no caching of tokens. The returned token can be used to authorize regular REST API calls that access the content of the document. For example, in a custom widget for a table with a Photos column containing attachments, the following code will update the src of an image with id the_image to show the attachment: grist.onRecord(async (record) => { const tokenInfo = await grist.docApi.getAccessToken({readOnly: true}); const img = document.getElementById('the_image'); const id = record.Photos[0]; // get an id of an attachment - there could be several // in a cell, for this example we just take the first. const src = `${tokenInfo.baseUrl}/attachments/${id}/download?auth=${tokenInfo.token}`; img.setAttribute('src', src); });","title":"getAccessToken"},{"location":"code/modules/grist_plugin_api/#parameters_2","text":"Name Type options? AccessTokenOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_4","text":"Promise < AccessTokenResult >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoption","text":"\u25b8 getOption ( key ): Promise < any > Get single value from Widget options object.","title":"getOption"},{"location":"code/modules/grist_plugin_api/#parameters_3","text":"Name Type key string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_5","text":"Promise < any >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#getoptions","text":"\u25b8 getOptions (): Promise < null | object > Gets all options stored by the widget. Options are stored as plain JSON object.","title":"getOptions"},{"location":"code/modules/grist_plugin_api/#returns_6","text":"Promise < null | object >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#gettable","text":"\u25b8 getTable ( tableId? ): TableOperations Get access to a table in the document. If no tableId specified, this will use the current selected table (for custom widgets). If a table does not exist, there will be no error until an operation on the table is attempted.","title":"getTable"},{"location":"code/modules/grist_plugin_api/#parameters_4","text":"Name Type tableId? string","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_7","text":"TableOperations","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnames","text":"\u25b8 mapColumnNames ( data , options? ): any Renames columns in the result using columns mapping configuration passed in ready method. Returns null if not all required columns were mapped or not widget doesn\u2019t support custom column mapping.","title":"mapColumnNames"},{"location":"code/modules/grist_plugin_api/#parameters_5","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap options.reverse? boolean","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_8","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#mapcolumnnamesback","text":"\u25b8 mapColumnNamesBack ( data , options? ): any Offer a convenient way to map data with renamed columns back into the form used in the original table. This is useful for making edits to the original table in a widget with column mappings. As for mapColumnNames(), we don\u2019t attempt to do these transformations automatically.","title":"mapColumnNamesBack"},{"location":"code/modules/grist_plugin_api/#parameters_6","text":"Name Type data any options? Object options.columns? ColumnsToMap options.mappings? null | WidgetColumnMap","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_9","text":"any","title":"Returns"},{"location":"code/modules/grist_plugin_api/#on","text":"\u25b8 on ( eventName , listener ): Rpc Adds the listener function to the end of the listeners array for the event named eventName . No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times. server.on('connection', (stream) => { console.log('someone connected!'); }); Returns a reference to the EventEmitter , so that calls can be chained. By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array. const myEE = new EventEmitter(); myEE.on('foo', () => console.log('a')); myEE.prependListener('foo', () => console.log('b')); myEE.emit('foo'); // Prints: // b // a Since v0.1.101","title":"on"},{"location":"code/modules/grist_plugin_api/#parameters_7","text":"Name Type Description eventName string | symbol The name of the event. listener (\u2026 args : any []) => void The callback function","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_10","text":"Rpc","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onnewrecord","text":"\u25b8 onNewRecord ( callback ): void For custom widgets, add a handler that will be called whenever the new (blank) row is selected.","title":"onNewRecord"},{"location":"code/modules/grist_plugin_api/#parameters_8","text":"Name Type callback ( mappings : null | WidgetColumnMap ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_11","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onoptions","text":"\u25b8 onOptions ( callback ): void For custom widgets, add a handler that will be called whenever the widget options change (and on initial ready message). Handler will be called with an object containing saved json options, or null if no options were saved. The second parameter has information about the widgets relationship with the document that contains it.","title":"onOptions"},{"location":"code/modules/grist_plugin_api/#parameters_9","text":"Name Type callback ( options : any , settings : InteractionOptions ) => unknown","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_12","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecord","text":"\u25b8 onRecord ( callback , options? ): void For custom widgets, add a handler that will be called whenever the row with the cursor changes - either by switching to a different row, or by some value within the row potentially changing. Handler may in the future be called with null if the cursor moves away from any row. By default, options.keepEncoded is false .","title":"onRecord"},{"location":"code/modules/grist_plugin_api/#parameters_10","text":"Name Type callback ( data : null | RowRecord , mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_13","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#onrecords","text":"\u25b8 onRecords ( callback , options? ): void For custom widgets, add a handler that will be called whenever the selected records change. By default, options.format is 'rows' and options.keepEncoded is false .","title":"onRecords"},{"location":"code/modules/grist_plugin_api/#parameters_11","text":"Name Type callback ( data : RowRecord [], mappings : null | WidgetColumnMap ) => unknown options FetchSelectedOptions","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_14","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#ready","text":"\u25b8 ready ( settings? ): void Declare that a component is prepared to receive messages from the outside world. Grist will not attempt to communicate with it until this method is called.","title":"ready"},{"location":"code/modules/grist_plugin_api/#parameters_12","text":"Name Type settings? ReadyPayload","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_15","text":"void","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setcursorpos","text":"\u25b8 setCursorPos ( pos ): Promise < void > Sets the cursor position to a specific row and field. sectionId is ignored. Used for widget linking.","title":"setCursorPos"},{"location":"code/modules/grist_plugin_api/#parameters_13","text":"Name Type pos CursorPos","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_16","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoption","text":"\u25b8 setOption ( key , value ): Promise < void > Store single value in the Widget options object (and create it if necessary).","title":"setOption"},{"location":"code/modules/grist_plugin_api/#parameters_14","text":"Name Type key string value any","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_17","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setoptions","text":"\u25b8 setOptions ( options ): Promise < void > Replaces all options stored by the widget.","title":"setOptions"},{"location":"code/modules/grist_plugin_api/#parameters_15","text":"Name Type options Object","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_18","text":"Promise < void >","title":"Returns"},{"location":"code/modules/grist_plugin_api/#setselectedrows","text":"\u25b8 setSelectedRows ( rowIds ): Promise < void > Set the list of selected rows to be used against any linked widget.","title":"setSelectedRows"},{"location":"code/modules/grist_plugin_api/#parameters_16","text":"Name Type rowIds null | number []","title":"Parameters"},{"location":"code/modules/grist_plugin_api/#returns_19","text":"Promise < void >","title":"Returns"},{"location":"examples/2020-06-book-club/","text":"Book Lists with Library and Store Look-ups # If there\u2019s one thing better than reading a book, it is reading with friends. To organize a book club is pretty simple. The club will work best if everyone actually gets the book, and has opinions for what to read next time. Grist can help with all that. To nudge the book into everyone\u2019s hands, it helps to have links to borrow the book at your local library, and to buy the book from your local (or not-so-local) store. And for ideas about what to read next time, it helps to be able to easily suggest books, and to learn about books others suggest via sites like GoodReads, Wikipedia, and Amazon. Suppose we start with a simple table of books, with book titles and author names. To borrow or buy the book, it is best to have its unique ISBN code, so there\u2019s no ambiguity or mix-ups. One thing we can do is add a link to search for a book on isbnsearch.org by its title and author. To do this, add a new column, and then set it to be a HyperLink column : What we\u2019re going to do is fill this column using a formula that takes the book title and the author\u2019s name, and uses them as a keyword to search on the isbnsearch.org site. Here\u2019s the formula: import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") \"[isbn] https://isbnsearch.org/search?s=\" + urllib.quote(keywords) URLs with spaces or apostrophes or other odd letters you might find in names need special encoding, so we\u2019ve used the python urllib module to make sure everything gets encoded correctly. Now we\u2019ve got a handy [isbn] link beside each book: We can click the arrow by [isbn] , locate the book, and make note of its ISBN code in a new column: Then we can add it into our book list. Now that we have a helping hand for finding ISBNs, let\u2019s add a few more books too: Library and store lookups # Once we have the ISBN, adding a link to buy the book is easy. Here\u2019s a formula for the indiebound.org site, which in the U.S. is likely to have an independent book store near you as a member: \"[buy] https://indiebound.org/book/\" + $ISBN If you don\u2019t have a local bookstore, there\u2019s Amazon, or pretty much any site you like (just find a search page on their site, and match the pattern). import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") + \" \" + ($ISBN or \"\") \"[amazon] https://www.amazon.com/s?i=stripbooks&k=\" + urllib.quote($keywords) The new links look like this: Clicking on the [buy] link near a book now brings us to that book on indiebound.org . For your local library, the same idea is very likely to work. For example, in northern New Jersey, in the US, here\u2019s what you want: import urllib prefix = \"[borrow] https://catalog.bccls.org/polaris/search/searchresults.aspx?ctx=placeholder&type=Keyword&by=ISBN&term=\" prefix + urllib.quote($ISBN) And here\u2019s how to look up Goodreads to see what people think of a book: \"[opinion] https://www.goodreads.com/search?q=\" + $ISBN And Wikipedia to start a deep dive: import urllib keywords = ($Title or \"\") + \" \" + ($Author.Name or \"\") \"[wikipedia] https://en.wikipedia.org/wiki/Special:Search/\" + urllib.quote(keywords) Once we have all these links, it makes sense to add a Card View so we can lay them out: Ready-made template # Here is an example book list that you can play with. The actual books listed may not be your thing, of course! Adjust to taste.","title":"Book club links"},{"location":"examples/2020-06-book-club/#book-lists-with-library-and-store-look-ups","text":"If there\u2019s one thing better than reading a book, it is reading with friends. To organize a book club is pretty simple. The club will work best if everyone actually gets the book, and has opinions for what to read next time. Grist can help with all that. To nudge the book into everyone\u2019s hands, it helps to have links to borrow the book at your local library, and to buy the book from your local (or not-so-local) store. And for ideas about what to read next time, it helps to be able to easily suggest books, and to learn about books others suggest via sites like GoodReads, Wikipedia, and Amazon. Suppose we start with a simple table of books, with book titles and author names. To borrow or buy the book, it is best to have its unique ISBN code, so there\u2019s no ambiguity or mix-ups. One thing we can do is add a link to search for a book on isbnsearch.org by its title and author. To do this, add a new column, and then set it to be a HyperLink column : What we\u2019re going to do is fill this column using a formula that takes the book title and the author\u2019s name, and uses them as a keyword to search on the isbnsearch.org site. Here\u2019s the formula: import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") \"[isbn] https://isbnsearch.org/search?s=\" + urllib.quote(keywords) URLs with spaces or apostrophes or other odd letters you might find in names need special encoding, so we\u2019ve used the python urllib module to make sure everything gets encoded correctly. Now we\u2019ve got a handy [isbn] link beside each book: We can click the arrow by [isbn] , locate the book, and make note of its ISBN code in a new column: Then we can add it into our book list. Now that we have a helping hand for finding ISBNs, let\u2019s add a few more books too:","title":"Book Lists with Library and Store Look-ups"},{"location":"examples/2020-06-book-club/#library-and-store-lookups","text":"Once we have the ISBN, adding a link to buy the book is easy. Here\u2019s a formula for the indiebound.org site, which in the U.S. is likely to have an independent book store near you as a member: \"[buy] https://indiebound.org/book/\" + $ISBN If you don\u2019t have a local bookstore, there\u2019s Amazon, or pretty much any site you like (just find a search page on their site, and match the pattern). import urllib keywords = ($Title or \"\") + \" \" + ($Author or \"\") + \" \" + ($ISBN or \"\") \"[amazon] https://www.amazon.com/s?i=stripbooks&k=\" + urllib.quote($keywords) The new links look like this: Clicking on the [buy] link near a book now brings us to that book on indiebound.org . For your local library, the same idea is very likely to work. For example, in northern New Jersey, in the US, here\u2019s what you want: import urllib prefix = \"[borrow] https://catalog.bccls.org/polaris/search/searchresults.aspx?ctx=placeholder&type=Keyword&by=ISBN&term=\" prefix + urllib.quote($ISBN) And here\u2019s how to look up Goodreads to see what people think of a book: \"[opinion] https://www.goodreads.com/search?q=\" + $ISBN And Wikipedia to start a deep dive: import urllib keywords = ($Title or \"\") + \" \" + ($Author.Name or \"\") \"[wikipedia] https://en.wikipedia.org/wiki/Special:Search/\" + urllib.quote(keywords) Once we have all these links, it makes sense to add a Card View so we can lay them out:","title":"Library and store lookups"},{"location":"examples/2020-06-book-club/#ready-made-template","text":"Here is an example book list that you can play with. The actual books listed may not be your thing, of course! Adjust to taste.","title":"Ready-made template"},{"location":"examples/2020-06-credit-card/","text":"Slicing and Dicing Expenses # Grist offers a handy way to explore your credit card transactions quickly, for example if you want to see transactions by: Category Card Member Month Combination of any of the above Here is an example using data from American Express (redacted for privacy, of course). See below for using it as a template for your own data. This first page is a dashboard: it shows a few summary charts \u2014 your expenses by month, and two pie charts to let you quickly see which card member or which category has the most expenses. The next three pages show breakdowns by Category, Card Member, and by Month. In addition to the totals by Category, you can click on any Category to see all transactions in it, and then click on any transaction to see its complete details. Here is the example with sample data that you can play with. To use it for your own data, start by downloading your transactions. For American Express credit cards: Log in at https://americanexpress.com Go to \u201cStatements & Activity\u201d tab, then select \u201cView By Year\u201d or \u201cCustom Date Range\u201d. Click \u201cDownload your Transactions\u201d icon. When asked to choose the download format, select \u201cCSV\u201d and select the checkbox that says \u201cInclude all additional transaction details\u201d. To import this data into Grist: Open the template here . Click \u201cAdd New\u201d button and choose \u201cImport from file\u201d. In the dialog that shows, change \u201cTo\u201d table from \u201cNew Table\u201d to \u201cActivity\u201d, like so: Click \u201cImport\u201d, and you are done. Your data is ready to explore. Note When you start with a template link, your copy of the document is initially unsaved. To keep this data for later, click \u201cSave Copy\u201d, and give the document a name. You\u2019ll see the document later on your Grist home page at https://docs.getgrist.com . A template like this doesn\u2019t take long to prepare. It uses a combination of summary tables and widget linking . Have feedback or improvements to contribute? Please share with us by emailing support@getgrist.com .","title":"Credit card expenses"},{"location":"examples/2020-06-credit-card/#slicing-and-dicing-expenses","text":"Grist offers a handy way to explore your credit card transactions quickly, for example if you want to see transactions by: Category Card Member Month Combination of any of the above Here is an example using data from American Express (redacted for privacy, of course). See below for using it as a template for your own data. This first page is a dashboard: it shows a few summary charts \u2014 your expenses by month, and two pie charts to let you quickly see which card member or which category has the most expenses. The next three pages show breakdowns by Category, Card Member, and by Month. In addition to the totals by Category, you can click on any Category to see all transactions in it, and then click on any transaction to see its complete details. Here is the example with sample data that you can play with. To use it for your own data, start by downloading your transactions. For American Express credit cards: Log in at https://americanexpress.com Go to \u201cStatements & Activity\u201d tab, then select \u201cView By Year\u201d or \u201cCustom Date Range\u201d. Click \u201cDownload your Transactions\u201d icon. When asked to choose the download format, select \u201cCSV\u201d and select the checkbox that says \u201cInclude all additional transaction details\u201d. To import this data into Grist: Open the template here . Click \u201cAdd New\u201d button and choose \u201cImport from file\u201d. In the dialog that shows, change \u201cTo\u201d table from \u201cNew Table\u201d to \u201cActivity\u201d, like so: Click \u201cImport\u201d, and you are done. Your data is ready to explore. Note When you start with a template link, your copy of the document is initially unsaved. To keep this data for later, click \u201cSave Copy\u201d, and give the document a name. You\u2019ll see the document later on your Grist home page at https://docs.getgrist.com . A template like this doesn\u2019t take long to prepare. It uses a combination of summary tables and widget linking . Have feedback or improvements to contribute? Please share with us by emailing support@getgrist.com .","title":"Slicing and Dicing Expenses"},{"location":"examples/2020-07-email-compose/","text":"Prepare Emails using Formulas # You may already know that you can add hyperlink fields in Grist. You may also know about \u201cmailto\u201d links that open a mail program to create a new email message. Less well-known is that \u201cmailto\u201d links allow pre-filling many parts of the email message. If you use Grist to store contacts, you can essentially create email templates using Grist formulas. Simple Mailto Links # The simplest \u201cmailto\u201d link in Grist looks like mailto:someone@example.com . When the column is set to Text, and its format is set to Hyperlink, it shows as someone@example.com . If you have a table with columns Full_Name and Email , add another column with this formula: \"Compose Email mailto:%s\" % ($Email) . Set its type to Text, and its cell format to Hyperlink: You\u2019ll get a link in each person\u2019s row, which you can click to start composing an email to that person: See an example of this in action here: Simple Compose . Cc, Bcc, Subject, Body # In addition to opening your email program and filling in the \u201cTO\u201d field, a \u201cmailto\u201d link can fill in other parts of the email, using this format: mailto:
'), )) This tells the formula what to use in place of the variable {Customer_Address} . Printing and Saving # Once your proposal is ready to go, you can print it or save it as a PDF. Click the three-dot icon at the upper-right of the widget then select \u2018Print widget\u2019. From here, you can either select a printer or choose \u2018Save as PDF\u2019 from the \u2018Destination\u2019 dropdown. Setting up multiple forms # You can add more form templates by following the same steps that we have just completed. Add a new template to the Templates table then build out the template using variables containing column IDs for any data that is project-specific. If you have some sections that are the same as another form, copy it over to save yourself the trouble of retyping! Create a dashboard where you can select a project and enter details for this form then preview the form in a custom widget. Don\u2019t forget, you\u2019ll need to add a formula column that combines the new form template with details for the selected project! This formula column is what you\u2019ll select under the \u2018Content\u2019 dropdown while configuring the Markdown Custom Widget .","title":"Proposals & Contracts"},{"location":"examples/2023-07-proposals-contracts/#creating-proposals","text":"If you are keeping business details and contracts in Grist, it can be convenient to generate proposals and contracts right there, alongside those records. You can use the Markdown Custom Widget to create a custom \u2018form\u2019 for Proposals, Contracts, or many other types of documents. This tutorial shows you how to set up a document like this: You can find a finished template here: \ud83d\udcdd Proposals & Contracts Template . If you\u2019d like to add a proposal to an existing document, understanding this tutorial should get you there.","title":"Creating Proposals"},{"location":"examples/2023-07-proposals-contracts/#setting-up-a-project-table","text":"First of all, make a table to record project details by creating an empty document and renaming Table1 to Projects : We\u2019ll create our Proposal template alongside our Projects table. We can insert column IDs as placeholders in our Proposal template that will then be replaced by the cell value for the selected project. For example, in the screenshot below, the value in the Project Name column will replace the variable {Project_Name} in the proposal template on the right. Seeing the available columns while creating our proposal will make it easier to populate those variables.","title":"Setting up a Project table"},{"location":"examples/2023-07-proposals-contracts/#creating-templates","text":"Let\u2019s add a new table, Templates , to the page to store our template data. Add two columns: Name and Template Formatting . Now, let\u2019s add a custom widget beside the table to view our Template Formatting . Click the green \u2018Add New\u2019 button then \u2018Add widget to page\u2019. Under \u2018Select Widget\u2019, select \u2018Custom\u2019 and under \u2018Select Data\u2019, select Templates . Under \u2018Select By\u2019, select Templates again. Configure the custom widget by selecting \u2018Markdown\u2019 from the \u2018Custom\u2019 dropdown. Since we will be editing the template directly in the custom widget, you must allow \u2018Full document access\u2019 under Access Level so the widget can update the Templates table. Under \u2018Content\u2019, select the column Template Formatting . This is the column that will be updated when we make edits within the custom widget. Create a Template in the Templates table by entering a value in the Name column. Then, start editing the template formatting within the custom widget. The widget uses Markdown formatting to format the text. For help on Markdown, click the ? at the top of the widget to view the Markdown Guide . When you click the \u2018save\u2019 icon, formatting from the widget will populate the Template Formatting column. We will exclusively use the custom widget to edit the template formatting so this column can be hidden from the table view. To hide the column, right-click on the column header then \u2018Hide column\u2019. In your template, you\u2019ll have details and text that remain the same across all projects such as formatting, section headers and your own company\u2019s information. That is the information you\u2019ll type directly into the template. You\u2019ll also have information that changes, such as Project Name or Customer Name . We can use variables containing column IDs as placeholders for that dynamic data. Project Name , Customer Name and Customer Address will all change based on the selected Project. So, this is information we should store in our Projects table. Add the columns Project Name , Customer Name and Customer Address to the Projects table. We can use the column IDs for each of these columns as placeholders in our template with the format {COLUMN_ID} . A column\u2019s ID can be found under the \u2018Table\u2019 tab of the Creator Panel, directly under the Column Label. Finish building out your template to fit your needs. Be sure to add a column to your Projects table for all variable information. Finally, we need to add a formula column that will create our unique proposals. This formula column will combine the template formatting we just created with our project-specifc data. Add a new column to the Projects table with the following formula: # Finds all data associated with this record class Find_Data(dict): def __missing__(self, key): return getattr(rec, key) # Finds the \"Proposal\" template in the Templates table template = Templates.lookupOne(Name=\"Proposal\").Template_Formatting # Formats the template with fields from this table as well as fields from the referenced table template.format_map(Find_Data())","title":"Creating templates"},{"location":"examples/2023-07-proposals-contracts/#setting-up-a-proposal-dashboard","text":"Next, we\u2019ll want to populate our proposal template with actual project data! Start creating a Proposal Dashboard by adding a new page to your document. Click the green \u2018Add New\u2019 button then \u2018Add Page\u2019. Under \u2018Select Widget\u2019, select \u2018Table\u2019 and under \u2018Select Data\u2019, select Projects . You\u2019ll notice that having this information in a table view is a bit busy. A Card widget will help simplify our view. Add a new widget to the page by clicking the green \u2018Add New\u2019 button then \u2018Add widget to page\u2019. Under \u2018Select Widget\u2019, select \u2018Card\u2019 and under \u2018Select Data\u2019, select Projects . Under \u2018Select By\u2019, select Projects and add to page. Your dashboard should look similar to the screenshot below. Now that we have all of our Project details in a Card view, we can hide them from our table view. Under the \u2018Table\u2019 tab of the Creator Panel, select all columns except Project Name and Customer Name then click the green \u2018Hide Columns\u2019 button. Keeping most project details in the Card widget, rather than the Table widget, simplifies our dashboard. You can easily see all projects in the Table widget, and when you want to see details for a specific project, select the project and the Card widget will update to show you project details. Finally, we\u2019ll want to add a view of our project-specific proposal. Add a new custom widget to the page by clicking the green \u2018Add New\u2019 button then \u2018Add widget to page\u2019. Under \u2018Select Widget\u2019, select \u2018Custom\u2019 and under \u2018Select Data\u2019, select Projects . Under \u2018Select By\u2019, select Projects and add to page. Configure the custom widget by selecting \u2018Markdown\u2019 from the \u2018Custom\u2019 dropdown. You must allow \u2018Full document access\u2019. Under \u2018Content\u2019, select the column Proposal . This is the formula column that combines our template formatting with our project-specific data. Customize your layout by rearranging and resizing widgets. Add project details for a new project and see how your proposal updates to display the newly added data.","title":"Setting up a proposal dashboard"},{"location":"examples/2023-07-proposals-contracts/#entering-customer-information","text":"Now, let\u2019s make two useful changes to the Project set-up: Put customer information in a separate table, so we don\u2019t have to re-enter their address every time we create a proposal for them (and we can import the addresses in bulk). Update the formula in the Proposal column of the Projects table to look for information in another table. First, create a new table called Customers for customer-specific information like Name and Address . Some of this data is included in our Projects data set. To avoid duplicating data, we need to update our Customer Name and Customer Address columns to pull from our Customers table. On our Proposals Dashboard page, select the Customer Name field then update the column type to Reference . Confirm that \u2018Data from Table\u2019 is set to Customers and \u2018Show Column\u2019 is Name . Next, we need to update the Customer Address field to pull the address for the customer listed in the Customer Name column. Update the Customer Address column to use the following formula: $Customer_Name.Address This formula uses our reference column, Customer , along with dot notation , to pull the value from the Address column of the referenced table. When you take a look at a proposal for an existing project, you\u2019ll notice that the Customer Name no longer populates. This is because of the way reference columns store data. Although under \u2018Show Column\u2019, we chose to see the value from the Name column of the referenced table, reference columns actually store a record\u2019s ID. That is what we are seeing now in the proposal. We can modify our formula in the Proposal column to look for data in other tables. In the Projects table, update the formula in the Proposals column to the following: # Finds all data associated with this record class Find_Data(dict): def __missing__(self, key): return getattr(rec, key) # Finds the \"Proposal\" template in the Templates table template = Templates.lookupOne(Name=\"Proposal\").Template_Formatting # Formats the template with fields from this table as well as fields from the referenced table template.format_map(Find_Data( Customer_Name = $Customer_Name.Name, )) In the last portion of the formula, we can specify variables that pull from other tables. Customer_Name = $Customer_Name.Name is for our reference column, Customer Name . It uses dot notation to specify what data to pull from the referenced table. Note: Customer Address The Customer Address column can be deleted from the Projects table completely. This data is already stored in the Customers table and our Customer Name column is a reference column pointing to this table. We can use this reference column to pull any other information from the Customers table to include in our proposal. If you choose to delete Customer Address from the Projects table, update the last section of formula to the following: template.format_map(Find_data( Customer_Name = $Customer_Name.Name, Customer_Address = $Customer_Name.Address.replace('\\n', '
'), )) This tells the formula what to use in place of the variable {Customer_Address} .","title":"Entering customer information"},{"location":"examples/2023-07-proposals-contracts/#printing-and-saving","text":"Once your proposal is ready to go, you can print it or save it as a PDF. Click the three-dot icon at the upper-right of the widget then select \u2018Print widget\u2019. From here, you can either select a printer or choose \u2018Save as PDF\u2019 from the \u2018Destination\u2019 dropdown.","title":"Printing and Saving"},{"location":"examples/2023-07-proposals-contracts/#setting-up-multiple-forms","text":"You can add more form templates by following the same steps that we have just completed. Add a new template to the Templates table then build out the template using variables containing column IDs for any data that is project-specific. If you have some sections that are the same as another form, copy it over to save yourself the trouble of retyping! Create a dashboard where you can select a project and enter details for this form then preview the form in a custom widget. Don\u2019t forget, you\u2019ll need to add a formula column that combines the new form template with details for the selected project! This formula column is what you\u2019ll select under the \u2018Content\u2019 dropdown while configuring the Markdown Custom Widget .","title":"Setting up multiple forms"},{"location":"install/aws-marketplace/","text":"AWS Marketplace # Grist on the AWS Marketplace has what you need to run a self-hosted Grist instance with minimal setup, and is based on grist-omnibus . Below are the complete configuration steps, including authentication via OpenID. First run setup # After deploying the instance, Grist should be instantly available through the HTTP protocol on an autogenerated domain such as ec2-3-94-254-105.compute-1.amazonaws.com (labeled Public IPv4 DNS by AWS). Default credentials: email: admin@example.getgrist.com password: [instance-id]* * Instance ID can be found on the EC2 page in the AWS Console: How to log in to the Grist instance # During deployment, you should have been asked about creating or using key pairs. You can use this pair to log in via SSH from your terminal/bash. The default user for the Grist EC2 instance is named \u201cubuntu\u201d, and you can log in to ubuntu@[ec2-instance-public-ip] . Note: You need to use the *.pem file you received while generating key pairs on AWS. Details about connecting via SSH can be found in the following places: Windows: https://learn.microsoft.com/en-us/windows/terminal/tutorials/ssh Linux: https://www.ssh.com/academy/ssh/command macOS: https://www.servermania.com/kb/articles/ssh-mac If you don\u2019t want to connect via SSH, AWS provides the option to connect from within the AWS console using the \u201cConnect\u201d button: Custom domain and SSL setup for HTTPS access # Custom domains are required for secure access to Grist. If you already have an SSL certificate, you can use your own (as described in the grist-omnibus README ). If not, Grist can generate a certificate from Let\u2019s Encrypt. For that, a valid domain and email must be configured: Point the domain to the IP address of the Grist EC2 instance. If you don\u2019t use the Elastic IP service , the instance can have a different public IPv4 address each time it\u2019s started. Log in to the Grist EC2 instance. Set the URL parameter in the grist/gristParameters file. You need administrator privileges to perform this action, so you can open an editor by running sudo nano grist/gristParameters . Run the restartGrist script with sudo ~/grist/restartGrist . Once the above steps are completed, you should be able to access Grist on your custom domain. Authentication setup # We support Google or Microsoft as OpenID providers. For configuring other authentication providers, please refer to the dex documentation . To configure Grist authentication with Google or Microsoft, you must have an application registered with the corresponding provider: Microsoft: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings Google: https://support.google.com/cloud/answer/6158849?hl=en Once you have your client ID and secret, you\u2019ll need to pass them to the gristParameters file inside the Grist EC2 instance: Log in to the Grist EC2 instance. Open ~/grist/gristParameters . Update the CLIENT_ID and CLIENT_SECRET sections for the relevant provider(s). If you\u2019re using only one provider, leave the second section commented out. Update ADMIN_EMAIL in the same file. It should correspond to the email you will use to log in via your authentication provider. For example: ADMIN_EMAIL=frank@your-organization.com If you want to change your team\u2019s name, update TEAM_NAME in the same file. Run restartGrist with the clean flag using sudo ~/grist/restartGrist clean to clear old login data. Important: This will delete all Grist documents! Once the above has been configured, you should be able to log in with your Google/Microsoft credentials. Running Grist in a separate VPC # grist-omnibus is designed to work on each account-default VPC. To make it run on a custom VPC, you\u2019ll need to properly configure all VPC elements. For more information on this configuration, read here . To run Grist on a VPC, the following must be properly set up: Assigning a public DNS name to the Grist EC2 instance is allowed. The VPC can be accessed from the internet (allowing internet gateway and routing tables to handle traffic). A security group connection from ports 22 (SSH for configuration), 80 (HTTP connection) and 433 (HTTPS connection) is allowed. Updating grist-omnibus # The packaged version of grist-omnibus will auto-update before each launch. To update grist-omnibus manually, restart the Grist EC2 instance or log in via SSH and call sudo ~/grist/restartGrist . There are currently no plans to support the Grist AWS Marketplace environment outside of grist-omnibus . Other important information # The Grist EC2 instance should have the \u201cPersistent store\u201d option checked. Grist stores all the data in the ~/grist-persist directory. Deleting this folder will result in a loss of all data from all documents. Do not delete ~/grist-persist/acme.json , as it contains a private key from Let\u2019s Encrypt. Deleting it too often can result in Let\u2019s Encrypt denying issuing further certificates from your domain.","title":"AWS Marketplace"},{"location":"install/aws-marketplace/#aws-marketplace","text":"Grist on the AWS Marketplace has what you need to run a self-hosted Grist instance with minimal setup, and is based on grist-omnibus . Below are the complete configuration steps, including authentication via OpenID.","title":"AWS Marketplace"},{"location":"install/aws-marketplace/#first-run-setup","text":"After deploying the instance, Grist should be instantly available through the HTTP protocol on an autogenerated domain such as ec2-3-94-254-105.compute-1.amazonaws.com (labeled Public IPv4 DNS by AWS). Default credentials: email: admin@example.getgrist.com password: [instance-id]* * Instance ID can be found on the EC2 page in the AWS Console:","title":"First run setup"},{"location":"install/aws-marketplace/#how-to-log-in-to-the-grist-instance","text":"During deployment, you should have been asked about creating or using key pairs. You can use this pair to log in via SSH from your terminal/bash. The default user for the Grist EC2 instance is named \u201cubuntu\u201d, and you can log in to ubuntu@[ec2-instance-public-ip] . Note: You need to use the *.pem file you received while generating key pairs on AWS. Details about connecting via SSH can be found in the following places: Windows: https://learn.microsoft.com/en-us/windows/terminal/tutorials/ssh Linux: https://www.ssh.com/academy/ssh/command macOS: https://www.servermania.com/kb/articles/ssh-mac If you don\u2019t want to connect via SSH, AWS provides the option to connect from within the AWS console using the \u201cConnect\u201d button:","title":"How to log in to the Grist instance"},{"location":"install/aws-marketplace/#custom-domain-and-ssl-setup-for-https-access","text":"Custom domains are required for secure access to Grist. If you already have an SSL certificate, you can use your own (as described in the grist-omnibus README ). If not, Grist can generate a certificate from Let\u2019s Encrypt. For that, a valid domain and email must be configured: Point the domain to the IP address of the Grist EC2 instance. If you don\u2019t use the Elastic IP service , the instance can have a different public IPv4 address each time it\u2019s started. Log in to the Grist EC2 instance. Set the URL parameter in the grist/gristParameters file. You need administrator privileges to perform this action, so you can open an editor by running sudo nano grist/gristParameters . Run the restartGrist script with sudo ~/grist/restartGrist . Once the above steps are completed, you should be able to access Grist on your custom domain.","title":"Custom domain and SSL setup for HTTPS access"},{"location":"install/aws-marketplace/#authentication-setup","text":"We support Google or Microsoft as OpenID providers. For configuring other authentication providers, please refer to the dex documentation . To configure Grist authentication with Google or Microsoft, you must have an application registered with the corresponding provider: Microsoft: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings Google: https://support.google.com/cloud/answer/6158849?hl=en Once you have your client ID and secret, you\u2019ll need to pass them to the gristParameters file inside the Grist EC2 instance: Log in to the Grist EC2 instance. Open ~/grist/gristParameters . Update the CLIENT_ID and CLIENT_SECRET sections for the relevant provider(s). If you\u2019re using only one provider, leave the second section commented out. Update ADMIN_EMAIL in the same file. It should correspond to the email you will use to log in via your authentication provider. For example: ADMIN_EMAIL=frank@your-organization.com If you want to change your team\u2019s name, update TEAM_NAME in the same file. Run restartGrist with the clean flag using sudo ~/grist/restartGrist clean to clear old login data. Important: This will delete all Grist documents! Once the above has been configured, you should be able to log in with your Google/Microsoft credentials.","title":"Authentication setup"},{"location":"install/aws-marketplace/#running-grist-in-a-separate-vpc","text":"grist-omnibus is designed to work on each account-default VPC. To make it run on a custom VPC, you\u2019ll need to properly configure all VPC elements. For more information on this configuration, read here . To run Grist on a VPC, the following must be properly set up: Assigning a public DNS name to the Grist EC2 instance is allowed. The VPC can be accessed from the internet (allowing internet gateway and routing tables to handle traffic). A security group connection from ports 22 (SSH for configuration), 80 (HTTP connection) and 433 (HTTPS connection) is allowed.","title":"Running Grist in a separate VPC"},{"location":"install/aws-marketplace/#updating-grist-omnibus","text":"The packaged version of grist-omnibus will auto-update before each launch. To update grist-omnibus manually, restart the Grist EC2 instance or log in via SSH and call sudo ~/grist/restartGrist . There are currently no plans to support the Grist AWS Marketplace environment outside of grist-omnibus .","title":"Updating grist-omnibus"},{"location":"install/aws-marketplace/#other-important-information","text":"The Grist EC2 instance should have the \u201cPersistent store\u201d option checked. Grist stores all the data in the ~/grist-persist directory. Deleting this folder will result in a loss of all data from all documents. Do not delete ~/grist-persist/acme.json , as it contains a private key from Let\u2019s Encrypt. Deleting it too often can result in Let\u2019s Encrypt denying issuing further certificates from your domain.","title":"Other important information"},{"location":"install/cloud-storage/","text":"Cloud Storage # This feature allows automatic syncing of Grist documents and document versions to S3-compatible stores such as MinIO (or AWS S3 itself). Grist Enterprise has native support for Azure storage accounts, and for AWS S3 using AWS\u2019s official client. It is advisable to have Redis enabled when using cloud storage, since this is the best-tested configuration. Enabling snapshotting results in a big change in how documents are stored, and is best done prior to creating documents. Back up your work before changing this configuration. S3-compatible stores via MinIO client # Turn this on by setting the following environment variables: Set GRIST_DOCS_MINIO_ACCESS_KEY and GRIST_DOCS_MINIO_SECRET_KEY . Set GRIST_DOCS_MINIO_BUCKET to the name of a versioned bucket you have created. It is important that the bucket have versioning enabled. Set GRIST_DOCS_MINIO_ENDPOINT to the appropriate hostname - no protocol, no port. (Optional) Set GRIST_DOCS_MINIO_USE_SSL to 1 to use https protocol (default) or 0 for http . (Optional) Set GRIST_DOCS_MINIO_PORT to the port to use, if the default for the protocol (80/443) isn\u2019t right. If using AWS S3, the endpoint to use is s3.amazonaws.com , and there\u2019s no need to set a port number or SSL flag. The access and secret keys are your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY . Make sure to create a versioned bucket in advance. Azure # For Azure: Create a storage account in the Azure portal. For the storage account\u2019s blob service, make sure that versioning is enabled. Get a connection string from the storage account\u2019s Access Keys section. It may look something like DefaultEndpointsProtocol=https;AccountName=... . Place the connection string in an environment variable called AZURE_STORAGE_CONNECTION_STRING . Set the name of an Azure storage container in an environment variable called GRIST_AZURE_CONTAINER . An example of a container name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_AZURE_PREFIX . S3 with native AWS client # For S3: Set the name of the S3 bucket in an environment variable called GRIST_DOCS_S3_BUCKET . An example of a bucket name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_DOCS_S3_PREFIX . Arrange for access using AWS\u2019s many options; if nothing else, you can set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables. Usage once configured # Once the external storage configuration is in place, start Grist as normal for self-managed Grist. Upon startup, there should be a line like: info: == grist.externalStorage.[s3|azure|minio].active: true All documents will be read from and saved to the corresponding S3 bucket or Azure container. Configuration is simplest on a fresh Grist install without any preexisting Grist documents. Once up and running, it is a good idea to configure the storage account\u2019s \u201clifecycle management\u201d to place any bounds you want on how long versions are retained. Grist has no requirements here, this is strictly to your taste.","title":"Cloud storage"},{"location":"install/cloud-storage/#cloud-storage","text":"This feature allows automatic syncing of Grist documents and document versions to S3-compatible stores such as MinIO (or AWS S3 itself). Grist Enterprise has native support for Azure storage accounts, and for AWS S3 using AWS\u2019s official client. It is advisable to have Redis enabled when using cloud storage, since this is the best-tested configuration. Enabling snapshotting results in a big change in how documents are stored, and is best done prior to creating documents. Back up your work before changing this configuration.","title":"Cloud Storage"},{"location":"install/cloud-storage/#s3-compatible-stores-via-minio-client","text":"Turn this on by setting the following environment variables: Set GRIST_DOCS_MINIO_ACCESS_KEY and GRIST_DOCS_MINIO_SECRET_KEY . Set GRIST_DOCS_MINIO_BUCKET to the name of a versioned bucket you have created. It is important that the bucket have versioning enabled. Set GRIST_DOCS_MINIO_ENDPOINT to the appropriate hostname - no protocol, no port. (Optional) Set GRIST_DOCS_MINIO_USE_SSL to 1 to use https protocol (default) or 0 for http . (Optional) Set GRIST_DOCS_MINIO_PORT to the port to use, if the default for the protocol (80/443) isn\u2019t right. If using AWS S3, the endpoint to use is s3.amazonaws.com , and there\u2019s no need to set a port number or SSL flag. The access and secret keys are your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY . Make sure to create a versioned bucket in advance.","title":"S3-compatible stores via MinIO client"},{"location":"install/cloud-storage/#azure","text":"For Azure: Create a storage account in the Azure portal. For the storage account\u2019s blob service, make sure that versioning is enabled. Get a connection string from the storage account\u2019s Access Keys section. It may look something like DefaultEndpointsProtocol=https;AccountName=... . Place the connection string in an environment variable called AZURE_STORAGE_CONNECTION_STRING . Set the name of an Azure storage container in an environment variable called GRIST_AZURE_CONTAINER . An example of a container name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_AZURE_PREFIX .","title":"Azure"},{"location":"install/cloud-storage/#s3-with-native-aws-client","text":"For S3: Set the name of the S3 bucket in an environment variable called GRIST_DOCS_S3_BUCKET . An example of a bucket name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_DOCS_S3_PREFIX . Arrange for access using AWS\u2019s many options; if nothing else, you can set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables.","title":"S3 with native AWS client"},{"location":"install/cloud-storage/#usage-once-configured","text":"Once the external storage configuration is in place, start Grist as normal for self-managed Grist. Upon startup, there should be a line like: info: == grist.externalStorage.[s3|azure|minio].active: true All documents will be read from and saved to the corresponding S3 bucket or Azure container. Configuration is simplest on a fresh Grist install without any preexisting Grist documents. Once up and running, it is a good idea to configure the storage account\u2019s \u201clifecycle management\u201d to place any bounds you want on how long versions are retained. Grist has no requirements here, this is strictly to your taste.","title":"Usage once configured"},{"location":"install/example-docker-nginx/","text":"Example using Docker and NGINX # This Example is originally authored by Akito . # This is a complete Docker setup example for Grist. The following docker-compose.yml files are needed. You will need to adjust the environment variables to your needs. Requirements # You need to have the most recent Docker distribution including the docker compose extension installed. To prepare the host environment, create an empty directory, and within it do: sudo -u \"$(id -un 1000):$(id -un 1000)\" mkdir -p ./config/nginx/site-confs ./data ./database/data NGINX Reverse Proxy with automatic HTTPS # For automatic HTTPS to work, you first need to setup proper DNS entries for the server you are running this reverse proxy on. This reverse proxy is decoupled from Grist, in a separate docker-compose.yml , so you may conveniently provide additional backends to which it can route traffic - for example, Authelia for authentication. This setup uses SWAG, a Docker image that bundles the NGINX reverse proxy with useful services including TLS certificate generation and renewal. This is the docker-compose.yml file managing the NGINX instance. version: \"3.9\" services: letsencrypt: image: lscr.io/linuxserver/swag # NGINX with automatic HTTPS container_name: nginx-letsencrypt-master network_mode: \"host\" environment: - PUID=1000 # Optional Change - PGID=1000 # Optional Change - TZ=Europe/London # Change! - URL=mydomain.eu # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - SUBDOMAINS=grist,webhook.grist # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - VALIDATION=http - EMAIL=admin@mydomain.eu # Change! - ONLY_SUBDOMAINS=true - STAGING=false # Enable if testing! volumes: - ./config:/config restart: unless-stopped NGINX Configuration # The following configuration is to be placed in ./config/nginx/site-confs/grist.conf , to make the NGINX instance route to Grist properly. server { listen 443 ssl http2; listen [::]:443 ssl http2; # Adjust to your needs! server_name grist.mydomain.eu webhook.grist.mydomain.eu; # enable subfolder method reverse proxy confs include /config/nginx/proxy-confs/*.subfolder.conf; # enable for ldap auth (requires ldap-location.conf in the location block) #include /config/nginx/ldap-server.conf; # enable for Authelia (requires authelia-location.conf in the location block) #include /config/nginx/authelia-server.conf; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } } Grist # This is the docker-compose.yml for the Grist backend. It contains the Grist app deployment, which is accompanied by a PostgreSQL database. # https://github.com/gristlabs/grist-core#using-grist version: \"3.9\" services: grist: image: gristlabs/grist:1.0.8 # Change! --> https://hub.docker.com/r/gristlabs/grist/tags container_name: grist user: \"1000\" # Optional Change env_file: - ./grist.env volumes: - ./data:/persist ports: - 127.0.0.1:3000:8080 depends_on: - database database: image: postgres:15-alpine container_name: grist_db user: \"1000\" # Optional Change env_file: - ./grist_db.env volumes: - ./database/data:/var/lib/postgresql/data Environment # The following .env files must be located in the same folder as the Grist docker-compose.yml . grist.env # # https://github.com/gristlabs/grist-core#environment-variables PORT=8080 APP_HOME_URL=https://grist.mydomain.eu GRIST_ALLOWED_HOSTS=webhook.grist.mydomain.eu # Replace with webhook target domains GRIST_DOMAIN=grist.mydomain.eu GRIST_SINGLE_ORG=myorg GRIST_HIDE_UI_ELEMENTS=billing GRIST_LIST_PUBLIC_SITES=false GRIST_MAX_UPLOAD_ATTACHMENT_MB=10 GRIST_MAX_UPLOAD_IMPORT_MB=300 GRIST_ORG_IN_PATH=false GRIST_PAGE_TITLE_SUFFIX=_blank GRIST_FORCE_LOGIN=true GRIST_SUPPORT_ANON=false GRIST_THROTTLE_CPU=true GRIST_SANDBOX_FLAVOR=gvisor PYTHON_VERSION=3 PYTHON_VERSION_ON_CREATION=3 # Database TYPEORM_DATABASE=grist TYPEORM_USERNAME=grist TYPEORM_HOST=grist_db TYPEORM_LOGGING=false TYPEORM_PASSWORD=mysupersecretpassword CHANGE THIS!!!! TYPEORM_PORT=5432 TYPEORM_TYPE=postgres grist_db.env # # https://hub.docker.com/_/postgres POSTGRES_DB=grist POSTGRES_USER=grist POSTGRES_PASSWORD=mysupersecretpassword CHANGE THIS!!!!","title":"Example using Docker and NGINX"},{"location":"install/example-docker-nginx/#example-using-docker-and-nginx","text":"","title":"Example using Docker and NGINX"},{"location":"install/example-docker-nginx/#this-example-is-originally-authored-by-akito","text":"This is a complete Docker setup example for Grist. The following docker-compose.yml files are needed. You will need to adjust the environment variables to your needs.","title":"This Example is originally authored by Akito."},{"location":"install/example-docker-nginx/#requirements","text":"You need to have the most recent Docker distribution including the docker compose extension installed. To prepare the host environment, create an empty directory, and within it do: sudo -u \"$(id -un 1000):$(id -un 1000)\" mkdir -p ./config/nginx/site-confs ./data ./database/data","title":"Requirements"},{"location":"install/example-docker-nginx/#nginx-reverse-proxy-with-automatic-https","text":"For automatic HTTPS to work, you first need to setup proper DNS entries for the server you are running this reverse proxy on. This reverse proxy is decoupled from Grist, in a separate docker-compose.yml , so you may conveniently provide additional backends to which it can route traffic - for example, Authelia for authentication. This setup uses SWAG, a Docker image that bundles the NGINX reverse proxy with useful services including TLS certificate generation and renewal. This is the docker-compose.yml file managing the NGINX instance. version: \"3.9\" services: letsencrypt: image: lscr.io/linuxserver/swag # NGINX with automatic HTTPS container_name: nginx-letsencrypt-master network_mode: \"host\" environment: - PUID=1000 # Optional Change - PGID=1000 # Optional Change - TZ=Europe/London # Change! - URL=mydomain.eu # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - SUBDOMAINS=grist,webhook.grist # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - VALIDATION=http - EMAIL=admin@mydomain.eu # Change! - ONLY_SUBDOMAINS=true - STAGING=false # Enable if testing! volumes: - ./config:/config restart: unless-stopped","title":"NGINX Reverse Proxy with automatic HTTPS"},{"location":"install/example-docker-nginx/#nginx-configuration","text":"The following configuration is to be placed in ./config/nginx/site-confs/grist.conf , to make the NGINX instance route to Grist properly. server { listen 443 ssl http2; listen [::]:443 ssl http2; # Adjust to your needs! server_name grist.mydomain.eu webhook.grist.mydomain.eu; # enable subfolder method reverse proxy confs include /config/nginx/proxy-confs/*.subfolder.conf; # enable for ldap auth (requires ldap-location.conf in the location block) #include /config/nginx/ldap-server.conf; # enable for Authelia (requires authelia-location.conf in the location block) #include /config/nginx/authelia-server.conf; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } }","title":"NGINX Configuration"},{"location":"install/example-docker-nginx/#grist","text":"This is the docker-compose.yml for the Grist backend. It contains the Grist app deployment, which is accompanied by a PostgreSQL database. # https://github.com/gristlabs/grist-core#using-grist version: \"3.9\" services: grist: image: gristlabs/grist:1.0.8 # Change! --> https://hub.docker.com/r/gristlabs/grist/tags container_name: grist user: \"1000\" # Optional Change env_file: - ./grist.env volumes: - ./data:/persist ports: - 127.0.0.1:3000:8080 depends_on: - database database: image: postgres:15-alpine container_name: grist_db user: \"1000\" # Optional Change env_file: - ./grist_db.env volumes: - ./database/data:/var/lib/postgresql/data","title":"Grist"},{"location":"install/example-docker-nginx/#environment","text":"The following .env files must be located in the same folder as the Grist docker-compose.yml .","title":"Environment"},{"location":"install/example-docker-nginx/#gristenv","text":"# https://github.com/gristlabs/grist-core#environment-variables PORT=8080 APP_HOME_URL=https://grist.mydomain.eu GRIST_ALLOWED_HOSTS=webhook.grist.mydomain.eu # Replace with webhook target domains GRIST_DOMAIN=grist.mydomain.eu GRIST_SINGLE_ORG=myorg GRIST_HIDE_UI_ELEMENTS=billing GRIST_LIST_PUBLIC_SITES=false GRIST_MAX_UPLOAD_ATTACHMENT_MB=10 GRIST_MAX_UPLOAD_IMPORT_MB=300 GRIST_ORG_IN_PATH=false GRIST_PAGE_TITLE_SUFFIX=_blank GRIST_FORCE_LOGIN=true GRIST_SUPPORT_ANON=false GRIST_THROTTLE_CPU=true GRIST_SANDBOX_FLAVOR=gvisor PYTHON_VERSION=3 PYTHON_VERSION_ON_CREATION=3 # Database TYPEORM_DATABASE=grist TYPEORM_USERNAME=grist TYPEORM_HOST=grist_db TYPEORM_LOGGING=false TYPEORM_PASSWORD=mysupersecretpassword CHANGE THIS!!!! TYPEORM_PORT=5432 TYPEORM_TYPE=postgres","title":"grist.env"},{"location":"install/example-docker-nginx/#grist_dbenv","text":"# https://hub.docker.com/_/postgres POSTGRES_DB=grist POSTGRES_USER=grist POSTGRES_PASSWORD=mysupersecretpassword CHANGE THIS!!!!","title":"grist_db.env"},{"location":"install/forwarded-headers/","text":"Forwarded Headers # You may have a middleware that does authentication and then passes identity on to web applications in a header. If you do, then Grist can be configured to respect that header. Warning The redirection logic for authentication using forwarded headers currently assumes a single team site configuration, and may misbehave for multi-site configurations. To make this work, here is what you\u2019ll need to do: Set GRIST_FORWARD_AUTH_HEADER to a header that will contain authorized user emails, say x-forwarded-user . This needs to match what your middleware will set. Make sure the /auth/login path is handled by your middleware before reaching Grist. Set GRIST_FORWARD_AUTH_LOGOUT_PATH to a path that will trigger a logout for your middleware (for example, /_oauth/logout ). Make sure that the logout path is handled by your middleware! If you want to allow anonymous access in some cases, make sure all other Grist paths are free of your middleware. Grist will trigger the middleware (by redirecting to /auth/login ) as needed. It\u2019s a good idea to strip GRIST_FORWARD_AUTH_HEADER from outside requests on all paths that aren\u2019t handled by your middleware. Your middleware may allow you to specify where to forward the user to after logging out. That should be /signed-out on the Grist site. Example: traefik-forward-auth # traefik-forward-auth is \u201cA minimal forward authentication service that provides OAuth/SSO login and authentication for the traefik reverse proxy/load balancer.\u201d The GRIST_FORWARD_AUTH_HEADER should be X-Forwarded-User , and this should be set in the authResponseHeaders settings for traefik. The GRIST_FORWARD_AUTH_LOGOUT_PATH should be /_oauth/logout , unless you have changed the default url-path setting for traefik-forward-auth. LOGOUT_REDIRECT for traefik-forward-auth should be https://DateTime:America/New_York
, Ref:Users
\",\"enum\":[\"Any\",\"Text\",\"Numeric\",\"Int\",\"Bool\",\"Date\",\"DateTime:$A + Table1.lookupOne(B=$B)
\"},\"isFormula\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column is a formula column. Use \\\"false\\\" for trigger formula column.\"},\"widgetOptions\":{\"type\":\"string\",\"description\":\"A JSON object with widget options, e.g.: {\\\"choices\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"alignment\\\": \\\"right\\\"}
\"},\"untieColIdFromLabel\":{\"type\":\"boolean\",\"description\":\"Use \\\"true\\\" to indicate that the column label should not be used as the column identifier. Use \\\"false\\\" to use the label as the identifier.\"},\"recalcWhen\":{\"type\":\"integer\",\"description\":\"A number indicating when the column should be recalculated.
\"},\"visibleCol\":{\"type\":\"integer\",\"description\":\"For Ref and RefList columns, the colRef of a column to display\"}}},\"CreateFields\":{\"allOf\":[{\"$ref\":\"#/components/schemas/Fields\"},{\"type\":\"object\",\"properties\":{\"recalcDeps\":{\"type\":\"string\",\"description\":\"An encoded array of column identifiers (colRefs) that this column depends on. If any of these columns change, the column will be recalculated. E.g.: [2, 3]
\"}}}]},\"GetFields\":{\"allOf\":[{\"$ref\":\"#/components/schemas/Fields\"},{\"type\":\"object\",\"properties\":{\"recalcDeps\":{\"type\":\"array\",\"items\":{\"type\":\"integer\"},\"description\":\"An array of column identifiers (colRefs) that this column depends on, prefixed with \\\"L\\\" constant. If any of these columns change, the column will be recalculated. E.g.: [\\\"L\\\", 2, 3]
\"},\"colRef\":{\"type\":\"integer\",\"description\":\"Column reference, e.g.: 2
\"}}}]},\"RowIds\":{\"type\":\"array\",\"example\":[101,102,103],\"items\":{\"type\":\"integer\"}},\"DocParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Competitive Analysis\"},\"isPinned\":{\"type\":\"boolean\",\"example\":false}}},\"WorkspaceParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"Retreat Docs\"}}},\"OrgParameters\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"example\":\"ACME Unlimited\"}}},\"OrgAccessRead\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"OrgAccessWrite\":{\"type\":\"object\",\"required\":[\"users\"],\"properties\":{\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"WorkspaceAccessRead\":{\"type\":\"object\",\"required\":[\"maxInheritedRole\",\"users\"],\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"name\"],\"properties\":{\"id\":{\"type\":\"integer\",\"example\":1},\"name\":{\"type\":\"string\",\"example\":\"Andrea\"},\"email\":{\"type\":\"string\",\"example\":\"andrea@getgrist.com\"},\"access\":{\"$ref\":\"#/components/schemas/Access\"},\"parentAccess\":{\"$ref\":\"#/components/schemas/Access\"}}}}}},\"WorkspaceAccessWrite\":{\"type\":\"object\",\"properties\":{\"maxInheritedRole\":{\"$ref\":\"#/components/schemas/Access\"},\"users\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"enum\":[\"owners\",\"editors\",\"viewers\",\"members\",null]},\"example\":{\"foo@getgrist.com\":\"owners\",\"bar@getgrist.com\":null}}}},\"DocAccessWrite\":{\"$ref\":\"#/components/schemas/WorkspaceAccessWrite\"},\"DocAccessRead\":{\"$ref\":\"#/components/schemas/WorkspaceAccessRead\"},\"AttachmentUpload\":{\"type\":\"object\",\"properties\":{\"upload\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"format\":\"binary\"}}}},\"AttachmentId\":{\"type\":\"number\",\"description\":\"An integer ID\"},\"AttachmentMetadata\":{\"type\":\"object\",\"properties\":{\"fileName\":{\"type\":\"string\",\"example\":\"logo.png\"},\"fileSize\":{\"type\":\"number\",\"example\":12345},\"timeUploaded\":{\"type\":\"string\",\"example\":\"2020-02-13T12:17:19.000Z\"}}},\"AttachmentMetadataList\":{\"type\":\"object\",\"required\":[\"records\"],\"properties\":{\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"fields\"],\"properties\":{\"id\":{\"type\":\"number\",\"example\":1},\"fields\":{\"$ref\":\"#/components/schemas/AttachmentMetadata\"}}}}}},\"SqlResultSet\":{\"type\":\"object\",\"required\":[\"statement\",\"records\"],\"properties\":{\"statement\":{\"type\":\"string\",\"description\":\"A copy of the SQL statement.\",\"example\":\"select * from Pets ...\"},\"records\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"object\"}}},\"example\":[{\"fields\":{\"id\":1,\"pet\":\"cat\",\"popularity\":67}},{\"fields\":{\"id\":2,\"pet\":\"dog\",\"popularity\":95}}]}}},\"TableSchemaResult\":{\"type\":\"object\",\"required\":[\"name\",\"title\",\"schema\"],\"properties\":{\"name\":{\"type\":\"string\",\"description\":\"The ID (technical name) of the table\"},\"title\":{\"type\":\"string\",\"description\":\"The human readable name of the table\"},\"path\":{\"type\":\"string\",\"description\":\"The URL to download the CSV\",\"example\":\"https://getgrist.com/o/docs/api/docs/ID/download/csv?tableId=Table1&....\"},\"format\":{\"type\":\"string\",\"enum\":[\"csv\"]},\"mediatype\":{\"type\":\"string\",\"enum\":[\"text/csv\"]},\"encoding\":{\"type\":\"string\",\"enum\":[\"utf-8\"]},\"dialect\":{\"$ref\":\"#/components/schemas/csv-dialect\"},\"schema\":{\"$ref\":\"#/components/schemas/table-schema\"}}},\"csv-dialect\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"CSV Dialect\",\"description\":\"The CSV dialect descriptor.\",\"type\":[\"string\",\"object\"],\"required\":[\"delimiter\",\"doubleQuote\"],\"properties\":{\"csvddfVersion\":{\"title\":\"CSV Dialect schema version\",\"description\":\"A number to indicate the schema version of CSV Dialect. Version 1.0 was named CSV Dialect Description Format and used different field names.\",\"type\":\"number\",\"default\":1.2,\"examples:\":[\"{\\n \\\"csvddfVersion\\\": \\\"1.2\\\"\\n}\\n\"]},\"delimiter\":{\"title\":\"Delimiter\",\"description\":\"A character sequence to use as the field separator.\",\"type\":\"string\",\"default\":\",\",\"examples\":[\"{\\n \\\"delimiter\\\": \\\",\\\"\\n}\\n\",\"{\\n \\\"delimiter\\\": \\\";\\\"\\n}\\n\"]},\"doubleQuote\":{\"title\":\"Double Quote\",\"description\":\"Specifies the handling of quotes inside fields.\",\"context\":\"If Double Quote is set to true, two consecutive quotes must be interpreted as one.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"doubleQuote\\\": true\\n}\\n\"]},\"lineTerminator\":{\"title\":\"Line Terminator\",\"description\":\"Specifies the character sequence that must be used to terminate rows.\",\"type\":\"string\",\"default\":\"\\r\\n\",\"examples\":[\"{\\n \\\"lineTerminator\\\": \\\"\\\\r\\\\n\\\"\\n}\\n\",\"{\\n \\\"lineTerminator\\\": \\\"\\\\n\\\"\\n}\\n\"]},\"nullSequence\":{\"title\":\"Null Sequence\",\"description\":\"Specifies the null sequence, for example, `\\\\N`.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"nullSequence\\\": \\\"\\\\N\\\"\\n}\\n\"]},\"quoteChar\":{\"title\":\"Quote Character\",\"description\":\"Specifies a one-character string to use as the quoting character.\",\"type\":\"string\",\"default\":\"\\\"\",\"examples\":[\"{\\n \\\"quoteChar\\\": \\\"'\\\"\\n}\\n\"]},\"escapeChar\":{\"title\":\"Escape Character\",\"description\":\"Specifies a one-character string to use as the escape character.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"escapeChar\\\": \\\"\\\\\\\\\\\"\\n}\\n\"]},\"skipInitialSpace\":{\"title\":\"Skip Initial Space\",\"description\":\"Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"skipInitialSpace\\\": true\\n}\\n\"]},\"header\":{\"title\":\"Header\",\"description\":\"Specifies if the file includes a header row, always as the first row in the file.\",\"type\":\"boolean\",\"default\":true,\"examples\":[\"{\\n \\\"header\\\": true\\n}\\n\"]},\"commentChar\":{\"title\":\"Comment Character\",\"description\":\"Specifies that any row beginning with this one-character string, without preceeding whitespace, causes the entire line to be ignored.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"commentChar\\\": \\\"#\\\"\\n}\\n\"]},\"caseSensitiveHeader\":{\"title\":\"Case Sensitive Header\",\"description\":\"Specifies if the case of headers is meaningful.\",\"context\":\"Use of case in source CSV files is not always an intentional decision. For example, should \\\"CAT\\\" and \\\"Cat\\\" be considered to have the same meaning.\",\"type\":\"boolean\",\"default\":false,\"examples\":[\"{\\n \\\"caseSensitiveHeader\\\": true\\n}\\n\"]}},\"examples\":[\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\";\\\"\\n }\\n}\\n\",\"{\\n \\\"dialect\\\": {\\n \\\"delimiter\\\": \\\"\\\\t\\\",\\n \\\"quoteChar\\\": \\\"'\\\",\\n \\\"commentChar\\\": \\\"#\\\"\\n }\\n}\\n\"]},\"table-schema\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Table Schema\",\"description\":\"A Table Schema for this resource, compliant with the [Table Schema](./tableschema/) specification.\",\"type\":[\"string\",\"object\"],\"required\":[\"fields\"],\"properties\":{\"fields\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Field\",\"type\":\"object\",\"oneOf\":[{\"type\":\"object\",\"title\":\"String Field\",\"description\":\"The field contains strings, that is, sequences of characters.\",\"required\":[\"name\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `string`.\",\"enum\":[\"string\"]},\"format\":{\"description\":\"The format keyword options for `string` are `default`, `email`, `uri`, `binary`, and `uuid`.\",\"context\":\"The following `format` options are supported:\\n * **default**: any valid string.\\n * **email**: A valid email address.\\n * **uri**: A valid URI.\\n * **binary**: A base64 encoded string representing binary data.\\n * **uuid**: A string that is a uuid.\",\"enum\":[\"default\",\"email\",\"uri\",\"binary\",\"uuid\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `string` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"pattern\":{\"type\":\"string\",\"description\":\"A regular expression pattern to test each value of the property against, where a truthy response indicates validity.\",\"context\":\"Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs).\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"name\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"constraints\\\": {\\n \\\"minLength\\\": 3,\\n \\\"maxLength\\\": 35\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Number Field\",\"description\":\"The field contains numbers of any kind including decimals.\",\"context\":\"The lexical formatting follows that of decimal in [XMLSchema](https://www.w3.org/TR/xmlschema-2/#decimal): a non-empty finite-length sequence of decimal digits separated by a period as a decimal indicator. An optional leading sign is allowed. If the sign is omitted, '+' is assumed. Leading and trailing zeroes are optional. If the fractional part is zero, the period and following zero(es) can be omitted. For example: '-1.23', '12678967.543233', '+100000.00', '210'.\\n\\nThe following special string values are permitted (case does not need to be respected):\\n - NaN: not a number\\n - INF: positive infinity\\n - -INF: negative infinity\\n\\nA number `MAY` also have a trailing:\\n - exponent: this `MUST` consist of an E followed by an optional + or - sign followed by one or more decimal digits (0-9)\\n - percentage: the percentage sign: `%`. In conversion percentages should be divided by 100.\\n\\nIf both exponent and percentages are present the percentage `MUST` follow the exponent e.g. '53E10%' (equals 5.3).\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `number`.\",\"enum\":[\"number\"]},\"format\":{\"description\":\"There are no format keyword options for `number`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"decimalChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to represent a decimal point within the number. The default value is `.`.\"},\"groupChar\":{\"type\":\"string\",\"description\":\"A string whose value is used to group digits within the number. The default value is `null`. A common value is `,` e.g. '100,000'.\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `number` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"number\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"number\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"field-name\\\",\\n \\\"type\\\": \\\"number\\\",\\n \\\"constraints\\\": {\\n \\\"enum\\\": [ \\\"1.00\\\", \\\"1.50\\\", \\\"2.00\\\" ]\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Integer Field\",\"description\":\"The field contains integers - that is whole numbers.\",\"context\":\"Integer values are indicated in the standard way for any valid integer.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `integer`.\",\"enum\":[\"integer\"]},\"format\":{\"description\":\"There are no format keyword options for `integer`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"bareNumber\":{\"type\":\"boolean\",\"title\":\"bareNumber\",\"description\":\"a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `\u20ac95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.\",\"default\":true},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `integer` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\",\\n \\\"constraints\\\": {\\n \\\"unique\\\": true,\\n \\\"minimum\\\": 100,\\n \\\"maximum\\\": 9999\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Field\",\"description\":\"The field contains temporal date values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `date`.\",\"enum\":[\"date\"]},\"format\":{\"description\":\"The format keyword options for `date` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string of YYYY-MM-DD.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `date` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": \\\"01-01-1900\\\"\\n }\\n}\\n\",\"{\\n \\\"name\\\": \\\"date_of_birth\\\",\\n \\\"type\\\": \\\"date\\\",\\n \\\"format\\\": \\\"MM-DD-YYYY\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Time Field\",\"description\":\"The field contains temporal time values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `time`.\",\"enum\":[\"time\"]},\"format\":{\"description\":\"The format keyword options for `time` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for time.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `time` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"appointment_start\\\",\\n \\\"type\\\": \\\"time\\\",\\n \\\"format\\\": \\\"any\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Date Time Field\",\"description\":\"The field contains temporal datetime values.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `datetime`.\",\"enum\":[\"datetime\"]},\"format\":{\"description\":\"The format keyword options for `datetime` are `default`, `any`, and `{PATTERN}`.\",\"context\":\"The following `format` options are supported:\\n * **default**: An ISO8601 format string for datetime.\\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).\",\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `datetime` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"timestamp\\\",\\n \\\"type\\\": \\\"datetime\\\",\\n \\\"format\\\": \\\"default\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Field\",\"description\":\"A calendar year, being an integer with 4 digits. Equivalent to [gYear in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYear)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `year`.\",\"enum\":[\"year\"]},\"format\":{\"description\":\"There are no format keyword options for `year`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `year` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"integer\"}}]},\"minimum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]},\"maximum\":{\"oneOf\":[{\"type\":\"string\"},{\"type\":\"integer\"}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"year\\\",\\n \\\"type\\\": \\\"year\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1970,\\n \\\"maximum\\\": 2003\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Year Month Field\",\"description\":\"A calendar year month, being an integer with 1 or 2 digits. Equivalent to [gYearMonth in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYearMonth)\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `yearmonth`.\",\"enum\":[\"yearmonth\"]},\"format\":{\"description\":\"There are no format keyword options for `yearmonth`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `yearmonth` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"month\\\",\\n \\\"type\\\": \\\"yearmonth\\\",\\n \\\"constraints\\\": {\\n \\\"minimum\\\": 1,\\n \\\"maximum\\\": 6\\n }\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Boolean Field\",\"description\":\"The field contains boolean (true/false) data.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `boolean`.\",\"enum\":[\"boolean\"]},\"format\":{\"description\":\"There are no format keyword options for `boolean`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"trueValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"true\",\"True\",\"TRUE\",\"1\"]},\"falseValues\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"type\":\"string\"},\"default\":[\"false\",\"False\",\"FALSE\",\"0\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `boolean` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"boolean\"}}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"registered\\\",\\n \\\"type\\\": \\\"boolean\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Object Field\",\"description\":\"The field contains data which can be parsed as a valid JSON object.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `object`.\",\"enum\":[\"object\"]},\"format\":{\"description\":\"There are no format keyword options for `object`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `object` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"extra\\\"\\n \\\"type\\\": \\\"object\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoPoint Field\",\"description\":\"The field contains data describing a geographic point.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geopoint`.\",\"enum\":[\"geopoint\"]},\"format\":{\"description\":\"The format keyword options for `geopoint` are `default`,`array`, and `object`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A string of the pattern 'lon, lat', where `lon` is the longitude and `lat` is the latitude.\\n * **array**: An array of exactly two items, where each item is either a number, or a string parsable as a number, and the first item is `lon` and the second item is `lat`.\\n * **object**: A JSON object with exactly two keys, `lat` and `lon`\",\"notes\":[\"Implementations `MUST` strip all white space in the default format of `lon, lat`.\"],\"enum\":[\"default\",\"array\",\"object\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geopoint` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"post_office\\\",\\n \\\"type\\\": \\\"geopoint\\\",\\n \\\"format\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"GeoJSON Field\",\"description\":\"The field contains a JSON object according to GeoJSON or TopoJSON\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `geojson`.\",\"enum\":[\"geojson\"]},\"format\":{\"description\":\"The format keyword options for `geojson` are `default` and `topojson`.\",\"context\":\"The following `format` options are supported:\\n * **default**: A geojson object as per the [GeoJSON spec](http://geojson.org/).\\n * **topojson**: A topojson object as per the [TopoJSON spec](https://github.com/topojson/topojson-specification/blob/master/README.md)\",\"enum\":[\"default\",\"topojson\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `geojson` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"object\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\"\\n}\\n\",\"{\\n \\\"name\\\": \\\"city_limits\\\",\\n \\\"type\\\": \\\"geojson\\\",\\n \\\"format\\\": \\\"topojson\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Array Field\",\"description\":\"The field contains data which can be parsed as a valid JSON array.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `array`.\",\"enum\":[\"array\"]},\"format\":{\"description\":\"There are no format keyword options for `array`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply for `array` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"array\"}}]},\"minLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the minimum length of a value.\"},\"maxLength\":{\"type\":\"integer\",\"description\":\"An integer that specifies the maximum length of a value.\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"options\\\"\\n \\\"type\\\": \\\"array\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Duration Field\",\"description\":\"The field contains a duration of time.\",\"context\":\"The lexical representation for duration is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) extended format `PnYnMnDTnHnMnS`, where `nY` represents the number of years, `nM` the number of months, `nD` the number of days, 'T' is the date/time separator, `nH` the number of hours, `nM` the number of minutes and `nS` the number of seconds. The number of seconds can include decimal digits to arbitrary precision. Date and time elements including their designator may be omitted if their value is zero, and lower order elements may also be omitted for reduced precision. Here we follow the definition of [XML Schema duration datatype](http://www.w3.org/TR/xmlschema-2/#duration) directly and that definition is implicitly inlined here.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `duration`.\",\"enum\":[\"duration\"]},\"format\":{\"description\":\"There are no format keyword options for `duration`: only `default` is allowed.\",\"enum\":[\"default\"],\"default\":\"default\"},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints are supported for `duration` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},\"minimum\":{\"type\":\"string\"},\"maximum\":{\"type\":\"string\"}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"period\\\"\\n \\\"type\\\": \\\"duration\\\"\\n}\\n\"]},{\"type\":\"object\",\"title\":\"Any Field\",\"description\":\"Any value is accepted, including values that are not captured by the type/format/constraint requirements of the specification.\",\"required\":[\"name\",\"type\"],\"properties\":{\"name\":{\"title\":\"Name\",\"description\":\"A name for this field.\",\"type\":\"string\"},\"title\":{\"title\":\"Title\",\"description\":\"A human-readable title.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"title\\\": \\\"My Package Title\\\"\\n}\\n\"]},\"description\":{\"title\":\"Description\",\"description\":\"A text description. Markdown is encouraged.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"description\\\": \\\"# My Package description\\\\nAll about my package.\\\"\\n}\\n\"]},\"example\":{\"title\":\"Example\",\"description\":\"An example value for the field.\",\"type\":\"string\",\"examples\":[\"{\\n \\\"example\\\": \\\"Put here an example value for your field\\\"\\n}\\n\"]},\"type\":{\"description\":\"The type keyword, which `MUST` be a value of `any`.\",\"enum\":[\"any\"]},\"constraints\":{\"title\":\"Constraints\",\"description\":\"The following constraints apply to `any` fields.\",\"type\":\"object\",\"properties\":{\"required\":{\"type\":\"boolean\",\"description\":\"Indicates whether a property must have a value for each instance.\",\"context\":\"An empty string is considered to be a missing value.\"},\"unique\":{\"type\":\"boolean\",\"description\":\"When `true`, each value for the property `MUST` be unique.\"},\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true}}},\"rdfType\":{\"type\":\"string\",\"description\":\"The RDF type for this field.\"}},\"examples\":[\"{\\n \\\"name\\\": \\\"notes\\\",\\n \\\"type\\\": \\\"any\\\"\\n\"]}]},\"description\":\"An `array` of Table Schema Field objects.\",\"examples\":[\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\"\\n }\\n ]\\n}\\n\",\"{\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"my-field-name\\\",\\n \\\"type\\\": \\\"number\\\"\\n },\\n {\\n \\\"name\\\": \\\"my-field-name-2\\\",\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"email\\\"\\n }\\n ]\\n}\\n\"]},\"primaryKey\":{\"oneOf\":[{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true,\"items\":{\"type\":\"string\"}},{\"type\":\"string\"}],\"description\":\"A primary key is a field name or an array of field names, whose values `MUST` uniquely identify each row in the table.\",\"context\":\"Field name in the `primaryKey` `MUST` be unique, and `MUST` match a field name in the associated table. It is acceptable to have an array with a single value, indicating that the value of a single field is the primary key.\",\"examples\":[\"{\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n}\\n\",\"{\\n \\\"primaryKey\\\": [\\n \\\"first_name\\\",\\n \\\"last_name\\\"\\n ]\\n}\\n\"]},\"foreignKeys\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"title\":\"Table Schema Foreign Key\",\"description\":\"Table Schema Foreign Key\",\"type\":\"object\",\"required\":[\"fields\",\"reference\"],\"oneOf\":[{\"properties\":{\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"minItems\":1,\"uniqueItems\":true,\"description\":\"Fields that make up the primary key.\"}},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"minItems\":1,\"uniqueItems\":true}}}}},{\"properties\":{\"fields\":{\"type\":\"string\",\"description\":\"Fields that make up the primary key.\"},\"reference\":{\"type\":\"object\",\"required\":[\"resource\",\"fields\"],\"properties\":{\"resource\":{\"type\":\"string\",\"default\":\"\"},\"fields\":{\"type\":\"string\"}}}}}]},\"examples\":[\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"the-resource\\\",\\n \\\"fields\\\": \\\"state_id\\\"\\n }\\n }\\n ]\\n}\\n\",\"{\\n \\\"foreignKeys\\\": [\\n {\\n \\\"fields\\\": \\\"state\\\",\\n \\\"reference\\\": {\\n \\\"resource\\\": \\\"\\\",\\n \\\"fields\\\": \\\"id\\\"\\n }\\n }\\n ]\\n}\\n\"]},\"missingValues\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"default\":[\"\"],\"description\":\"Values that when encountered in the source, should be considered as `null`, 'not present', or 'blank' values.\",\"context\":\"Many datasets arrive with missing data values, either because a value was not collected or it never existed.\\nMissing values may be indicated simply by the value being empty in other cases a special value may have been used e.g. `-`, `NaN`, `0`, `-9999` etc.\\nThe `missingValues` property provides a way to indicate that these values should be interpreted as equivalent to null.\\n\\n`missingValues` are strings rather than being the data type of the particular field. This allows for comparison prior to casting and for fields to have missing value which are not of their type, for example a `number` field to have missing values indicated by `-`.\\n\\nThe default value of `missingValue` for a non-string type field is the empty string `''`. For string type fields there is no default for `missingValue` (for string fields the empty string `''` is a valid value and need not indicate null).\",\"examples\":[\"{\\n \\\"missingValues\\\": [\\n \\\"-\\\",\\n \\\"NaN\\\",\\n \\\"\\\"\\n ]\\n}\\n\",\"{\\n \\\"missingValues\\\": []\\n}\\n\"]}},\"examples\":[\"{\\n \\\"schema\\\": {\\n \\\"fields\\\": [\\n {\\n \\\"name\\\": \\\"first_name\\\",\\n \\\"type\\\": \\\"string\\\"\\n \\\"constraints\\\": {\\n \\\"required\\\": true\\n }\\n },\\n {\\n \\\"name\\\": \\\"age\\\",\\n \\\"type\\\": \\\"integer\\\"\\n },\\n ],\\n \\\"primaryKey\\\": [\\n \\\"name\\\"\\n ]\\n }\\n}\\n\"]}},\"parameters\":{\"filterQueryParam\":{\"in\":\"query\",\"name\":\"filter\",\"schema\":{\"type\":\"string\"},\"description\":\"This is a JSON object mapping column names to arrays of allowed values. For example, to filter column `pet` for values `cat` and `dog`, the filter would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}`. JSON contains characters that are not safe to place in a URL, so it is important to url-encode them. For this example, the url-encoding is `%7B%22pet%22%3A%20%5B%22cat%22%2C%20%22dog%22%5D%7D`. See https://rosettacode.org/wiki/URL_encoding for how to url-encode a string, or https://www.urlencoder.org/ to try some examples. Multiple columns can be filtered. For example the filter for `pet` being either `cat` or `dog`, AND `size` being either `tiny` or `outrageously small`, would be `{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"], \\\"size\\\": [\\\"tiny\\\", \\\"outrageously small\\\"]}`.\",\"example\":\"{\\\"pet\\\": [\\\"cat\\\", \\\"dog\\\"]}\",\"required\":false},\"sortQueryParam\":{\"in\":\"query\",\"name\":\"sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Order in which to return results. If a single column name is given (e.g. `pet`), results are placed in ascending order of values in that column. To get results in an order that was previously prepared manually in Grist, use the special `manualSort` column name. Multiple columns can be specified, separated by commas (e.g. `pet,age`). For descending order, prefix a column name with a `-` character (e.g. `pet,-age`). To include additional sorting options append them after a colon (e.g. `pet,-age:naturalSort;emptyFirst,owner`). Available options are: `choiceOrder`, `naturalSort`, `emptyFirst`. Without the `sort` parameter, the order of results is unspecified.\",\"example\":\"pet,-age\",\"required\":false},\"limitQueryParam\":{\"in\":\"query\",\"name\":\"limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Return at most this number of rows. A value of 0 is equivalent to having no limit.\",\"example\":\"5\",\"required\":false},\"sortHeaderParam\":{\"in\":\"header\",\"name\":\"X-Sort\",\"schema\":{\"type\":\"string\"},\"description\":\"Same as `sort` query parameter.\",\"example\":\"pet,-age\",\"required\":false},\"limitHeaderParam\":{\"in\":\"header\",\"name\":\"X-Limit\",\"schema\":{\"type\":\"number\"},\"description\":\"Same as `limit` query parameter.\",\"example\":\"5\",\"required\":false},\"colIdPathParam\":{\"in\":\"path\",\"name\":\"colId\",\"schema\":{\"type\":\"string\"},\"description\":\"The column id (without the starting `$`) as shown in the column configuration below the label\",\"required\":true},\"tableIdPathParam\":{\"in\":\"path\",\"name\":\"tableId\",\"schema\":{\"type\":\"string\"},\"description\":\"normalized table name (see `TABLE ID` in Raw Data) or numeric row ID in `_grist_Tables`\",\"required\":true},\"docIdPathParam\":{\"in\":\"path\",\"name\":\"docId\",\"schema\":{\"type\":\"string\"},\"description\":\"A string id (UUID)\",\"required\":true},\"orgIdPathParam\":{\"in\":\"path\",\"name\":\"orgId\",\"schema\":{\"oneOf\":[{\"type\":\"integer\"},{\"type\":\"string\"}]},\"description\":\"This can be an integer id, or a string subdomain (e.g. `gristlabs`), or `current` if the org is implied by the domain in the url\",\"required\":true},\"workspaceIdPathParam\":{\"in\":\"path\",\"name\":\"workspaceId\",\"description\":\"An integer id\",\"schema\":{\"type\":\"integer\"},\"required\":true},\"noparseQueryParam\":{\"in\":\"query\",\"name\":\"noparse\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to prohibit parsing strings according to the column type.\"},\"hiddenQueryParam\":{\"in\":\"query\",\"name\":\"hidden\",\"schema\":{\"type\":\"boolean\"},\"description\":\"Set to true to include the hidden columns (like \\\"manualSort\\\")\"},\"headerQueryParam\":{\"in\":\"query\",\"name\":\"header\",\"schema\":{\"type\":\"string\",\"enum\":[\"colId\",\"label\"]},\"description\":\"Format for headers. Labels tend to be more human-friendly while colIds are more normalized.\",\"required\":false}}}}},\"searchIndex\":{\"store\":[\"section/Authentication\",\"tag/orgs\",\"tag/orgs/operation/listOrgs\",\"tag/orgs/operation/describeOrg\",\"tag/orgs/operation/modifyOrg\",\"tag/orgs/operation/listOrgAccess\",\"tag/orgs/operation/modifyOrgAccess\",\"tag/workspaces\",\"tag/workspaces/operation/listWorkspaces\",\"tag/workspaces/operation/createWorkspace\",\"tag/workspaces/operation/describeWorkspace\",\"tag/workspaces/operation/modifyWorkspace\",\"tag/workspaces/operation/deleteWorkspace\",\"tag/workspaces/operation/listWorkspaceAccess\",\"tag/workspaces/operation/modifyWorkspaceAccess\",\"tag/docs\",\"tag/docs/operation/createDoc\",\"tag/docs/operation/describeDoc\",\"tag/docs/operation/modifyDoc\",\"tag/docs/operation/deleteDoc\",\"tag/docs/operation/moveDoc\",\"tag/docs/operation/listDocAccess\",\"tag/docs/operation/modifyDocAccess\",\"tag/docs/operation/downloadDoc\",\"tag/docs/operation/downloadDocXlsx\",\"tag/docs/operation/downloadDocCsv\",\"tag/docs/operation/downloadTableSchema\",\"tag/records\",\"tag/records/operation/listRecords\",\"tag/records/operation/addRecords\",\"tag/records/operation/modifyRecords\",\"tag/records/operation/replaceRecords\",\"tag/tables\",\"tag/tables/operation/listTables\",\"tag/tables/operation/addTables\",\"tag/tables/operation/modifyTables\",\"tag/columns\",\"tag/columns/operation/listColumns\",\"tag/columns/operation/addColumns\",\"tag/columns/operation/modifyColumns\",\"tag/columns/operation/replaceColumns\",\"tag/columns/operation/deleteColumn\",\"tag/data\",\"tag/data/operation/getTableData\",\"tag/data/operation/addRows\",\"tag/data/operation/modifyRows\",\"tag/data/operation/deleteRows\",\"tag/attachments\",\"tag/attachments/operation/listAttachments\",\"tag/attachments/operation/uploadAttachments\",\"tag/attachments/operation/getAttachmentMetadata\",\"tag/attachments/operation/downloadAttachment\",\"tag/webhooks\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/get\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks/post\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/patch\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1{webhookId}/delete\",\"tag/webhooks/paths/~1docs~1{docId}~1webhooks~1queue/delete\",\"tag/sql\",\"tag/sql/paths/~1docs~1{docId}~1sql/get\",\"tag/sql/paths/~1docs~1{docId}~1sql/post\"],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"title\",\"description\"],\"fieldVectors\":[[\"title/0\",[0,5.068]],[\"description/0\",[1,4.208,2,4.208]],[\"title/1\",[3,2.418]],[\"description/1\",[3,1.097,4,1.984,5,1.776,6,1.984,7,2.3,8,1.621,9,2.3]],[\"title/2\",[3,1.739,10,1.946,11,2.068]],[\"description/2\",[3,1.097,4,1.984,5,1.776,6,1.984,12,2.3,13,2.3,14,2.3]],[\"title/3\",[3,2.023,15,3.275]],[\"description/3\",[16,4.353]],[\"title/4\",[3,2.023,17,2.264]],[\"description/4\",[16,4.353]],[\"title/5\",[3,1.525,10,1.706,11,1.814,18,2.468]],[\"description/5\",[19,4.353]],[\"title/6\",[3,1.739,11,2.068,20,2.569]],[\"description/6\",[19,4.353]],[\"title/7\",[21,2.294]],[\"description/7\",[5,1.953,8,1.782,21,1.145,22,2.529,23,2.529,24,0.689]],[\"title/8\",[3,1.358,10,1.519,21,1.288,24,0.775,25,2.845]],[\"description/8\",[26,4.353]],[\"title/9\",[21,1.65,27,2.815,28,2.815]],[\"description/9\",[26,4.353]],[\"title/10\",[15,3.275,21,1.919]],[\"description/10\",[29,3.896]],[\"title/11\",[17,2.264,21,1.919]],[\"description/11\",[29,3.896]],[\"title/12\",[21,1.919,30,2.988]],[\"description/12\",[29,3.896]],[\"title/13\",[10,1.706,11,1.814,18,2.468,21,1.447]],[\"description/13\",[31,4.353]],[\"title/14\",[11,2.068,20,2.569,21,1.65]],[\"description/14\",[31,4.353]],[\"title/15\",[32,3.914]],[\"description/15\",[21,1.272,24,0.765,33,2.424,34,1.98,35,2.809]],[\"title/16\",[24,0.993,27,2.815,28,2.815]],[\"description/16\",[36,5.045]],[\"title/17\",[15,3.275,24,1.155]],[\"description/17\",[37,3.896]],[\"title/18\",[17,1.706,24,0.87,38,2.468,39,2.08]],[\"description/18\",[37,3.896]],[\"title/19\",[24,1.155,30,2.988]],[\"description/19\",[37,3.896]],[\"title/20\",[3,1.223,21,1.161,24,0.698,40,2.564,41,2.564,42,2.564]],[\"description/20\",[43,5.045]],[\"title/21\",[10,1.706,11,1.814,18,2.468,24,0.87]],[\"description/21\",[44,4.353]],[\"title/22\",[11,2.068,20,2.569,24,0.993]],[\"description/22\",[44,4.353]],[\"title/23\",[24,0.87,39,2.08,45,3.196,46,2.252]],[\"description/23\",[47,5.045]],[\"title/24\",[24,0.87,39,2.08,46,2.252,48,3.196]],[\"description/24\",[49,5.045]],[\"title/25\",[39,2.08,46,2.252,50,0.833,51,3.196]],[\"description/25\",[52,5.045]],[\"title/26\",[50,1.105,53,3.658]],[\"description/26\",[53,2.182,54,2.529,55,2.529,56,2.529,57,2.529,58,2.529]],[\"title/27\",[59,2.294]],[\"description/27\",[8,1.782,33,2.182,34,1.782,50,0.659,59,1.145,60,1.782]],[\"title/28\",[50,0.95,59,1.65,61,3.144]],[\"description/28\",[62,3.556]],[\"title/29\",[50,0.95,59,1.65,63,2.209]],[\"description/29\",[62,3.556]],[\"title/30\",[17,1.946,50,0.95,59,1.65]],[\"description/30\",[62,3.556]],[\"title/31\",[50,0.833,59,1.447,63,1.937,64,2.757]],[\"description/31\",[62,3.556]],[\"title/32\",[50,1.321]],[\"description/32\",[24,0.86,34,2.227,50,0.823,65,2.726]],[\"title/33\",[10,1.946,24,0.993,50,0.95]],[\"description/33\",[66,3.896]],[\"title/34\",[24,0.993,50,0.95,63,2.209]],[\"description/34\",[66,3.896]],[\"title/35\",[17,1.946,24,0.993,50,0.95]],[\"description/35\",[66,3.896]],[\"title/36\",[67,2.706]],[\"description/36\",[34,2.227,50,0.823,65,2.726,67,1.687]],[\"title/37\",[10,1.946,50,0.95,67,1.946]],[\"description/37\",[68,3.556]],[\"title/38\",[50,0.95,63,2.209,67,1.946]],[\"description/38\",[68,3.556]],[\"title/39\",[17,1.946,50,0.95,67,1.946]],[\"description/39\",[68,3.556]],[\"title/40\",[50,0.833,63,1.937,64,2.757,67,1.706]],[\"description/40\",[68,3.556]],[\"title/41\",[30,2.569,50,0.95,67,1.946]],[\"description/41\",[69,5.045]],[\"title/42\",[70,3.572]],[\"description/42\",[50,0.412,59,0.716,70,1.115,71,1.582,72,1.365,73,2.652,74,1.115,75,1.582,76,1.582,77,1.582,78,1.115]],[\"title/43\",[50,0.95,61,3.144,70,2.569]],[\"description/43\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/44\",[50,0.95,60,2.569,63,2.209]],[\"description/44\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/45\",[17,1.946,50,0.95,60,2.569]],[\"description/45\",[59,0.993,74,0.892,78,1.546,79,0.978,80,0.978,81,0.978,82,0.892,83,0.978,84,0.978,85,0.978,86,0.978,87,0.892,88,0.978,89,0.978]],[\"title/46\",[30,2.569,50,0.95,60,2.569]],[\"description/46\",[90,5.045]],[\"title/47\",[91,3.071]],[\"description/47\",[24,0.46,46,1.189,59,0.764,67,0.901,70,1.189,72,1.456,91,1.696,92,1.687,93,1.687,94,1.687]],[\"title/48\",[10,1.706,32,2.468,38,2.468,91,1.937]],[\"description/48\",[95,4.353]],[\"title/49\",[32,2.815,91,2.209,96,3.645]],[\"description/49\",[95,4.353]],[\"title/50\",[38,3.275,91,2.569]],[\"description/50\",[97,5.045]],[\"title/51\",[39,2.372,91,2.209,98,3.645]],[\"description/51\",[99,5.045]],[\"title/52\",[100,3.071]],[\"description/52\",[8,1.621,20,1.621,24,0.626,100,1.394,101,2.3,102,2.3,103,2.3]],[\"title/53\",[24,0.993,100,2.209,104,3.645]],[\"description/53\",[105,4.353]],[\"title/54\",[24,0.87,27,2.468,87,2.252,100,1.937]],[\"description/54\",[105,4.353]],[\"title/55\",[17,2.264,100,2.569]],[\"description/55\",[106,4.353]],[\"title/56\",[82,2.988,100,2.569]],[\"description/56\",[106,4.353]],[\"title/57\",[28,2.198,107,2.845,108,2.845,109,2.845,110,2.845]],[\"description/57\",[111,5.045]],[\"title/58\",[112,3.914]],[\"description/58\",[]],[\"title/59\",[24,0.775,112,2.198,113,2.455,114,2.455,115,2.455]],[\"description/59\",[116,4.353]],[\"title/60\",[24,0.636,112,1.802,113,2.013,114,2.013,115,2.013,117,2.334,118,2.334]],[\"description/60\",[116,4.353]]],\"invertedIndex\":[[\"\",{\"_index\":2,\"title\":{},\"description\":{\"0\":{}}}],[\"access\",{\"_index\":11,\"title\":{\"2\":{},\"5\":{},\"6\":{},\"13\":{},\"14\":{},\"21\":{},\"22\":{}},\"description\":{}}],[\"add\",{\"_index\":63,\"title\":{\"29\":{},\"31\":{},\"34\":{},\"38\":{},\"40\":{},\"44\":{}},\"description\":{}}],[\"against\",{\"_index\":115,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"anoth\",{\"_index\":41,\"title\":{\"20\":{}},\"description\":{}}],[\"api\",{\"_index\":9,\"title\":{},\"description\":{\"1\":{}}}],[\"area\",{\"_index\":13,\"title\":{},\"description\":{\"2\":{}}}],[\"associ\",{\"_index\":104,\"title\":{\"53\":{}},\"description\":{}}],[\"attach\",{\"_index\":91,\"title\":{\"47\":{},\"48\":{},\"49\":{},\"50\":{},\"51\":{}},\"description\":{\"47\":{}}}],[\"authent\",{\"_index\":0,\"title\":{\"0\":{}},\"description\":{}}],[\"avail\",{\"_index\":14,\"title\":{},\"description\":{\"2\":{}}}],[\"better\",{\"_index\":84,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"call\",{\"_index\":8,\"title\":{},\"description\":{\"1\":{},\"7\":{},\"27\":{},\"52\":{}}}],[\"chang\",{\"_index\":20,\"title\":{\"6\":{},\"14\":{},\"22\":{}},\"description\":{\"52\":{}}}],[\"collect\",{\"_index\":34,\"title\":{},\"description\":{\"15\":{},\"27\":{},\"32\":{},\"36\":{}}}],[\"column\",{\"_index\":67,\"title\":{\"36\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{}},\"description\":{\"36\":{},\"47\":{}}}],[\"columnar\",{\"_index\":75,\"title\":{},\"description\":{\"42\":{}}}],[\"consid\",{\"_index\":83,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"contain\",{\"_index\":33,\"title\":{},\"description\":{\"15\":{},\"27\":{}}}],[\"content\",{\"_index\":39,\"title\":{\"18\":{},\"23\":{},\"24\":{},\"25\":{},\"51\":{}},\"description\":{}}],[\"creat\",{\"_index\":27,\"title\":{\"9\":{},\"16\":{},\"54\":{}},\"description\":{}}],[\"csv\",{\"_index\":51,\"title\":{\"25\":{}},\"description\":{}}],[\"data\",{\"_index\":70,\"title\":{\"42\":{},\"43\":{}},\"description\":{\"42\":{},\"47\":{}}}],[\"delet\",{\"_index\":30,\"title\":{\"12\":{},\"19\":{},\"41\":{},\"46\":{}},\"description\":{}}],[\"deprec\",{\"_index\":74,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"describ\",{\"_index\":15,\"title\":{\"3\":{},\"10\":{},\"17\":{}},\"description\":{}}],[\"doc\",{\"_index\":32,\"title\":{\"15\":{},\"48\":{},\"49\":{}},\"description\":{}}],[\"docs/{docid\",{\"_index\":37,\"title\":{},\"description\":{\"17\":{},\"18\":{},\"19\":{}}}],[\"docs/{docid}/access\",{\"_index\":44,\"title\":{},\"description\":{\"21\":{},\"22\":{}}}],[\"docs/{docid}/attach\",{\"_index\":95,\"title\":{},\"description\":{\"48\":{},\"49\":{}}}],[\"docs/{docid}/attachments/{attachmentid\",{\"_index\":97,\"title\":{},\"description\":{\"50\":{}}}],[\"docs/{docid}/attachments/{attachmentid}/download\",{\"_index\":99,\"title\":{},\"description\":{\"51\":{}}}],[\"docs/{docid}/download\",{\"_index\":47,\"title\":{},\"description\":{\"23\":{}}}],[\"docs/{docid}/download/csv\",{\"_index\":52,\"title\":{},\"description\":{\"25\":{}}}],[\"docs/{docid}/download/table-schema\",{\"_index\":58,\"title\":{},\"description\":{\"26\":{}}}],[\"docs/{docid}/download/xlsx\",{\"_index\":49,\"title\":{},\"description\":{\"24\":{}}}],[\"docs/{docid}/mov\",{\"_index\":43,\"title\":{},\"description\":{\"20\":{}}}],[\"docs/{docid}/sql\",{\"_index\":116,\"title\":{},\"description\":{\"59\":{},\"60\":{}}}],[\"docs/{docid}/t\",{\"_index\":66,\"title\":{},\"description\":{\"33\":{},\"34\":{},\"35\":{}}}],[\"docs/{docid}/tables/{tableid}/column\",{\"_index\":68,\"title\":{},\"description\":{\"37\":{},\"38\":{},\"39\":{},\"40\":{}}}],[\"docs/{docid}/tables/{tableid}/columns/{colid\",{\"_index\":69,\"title\":{},\"description\":{\"41\":{}}}],[\"docs/{docid}/tables/{tableid}/data\",{\"_index\":89,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"docs/{docid}/tables/{tableid}/data/delet\",{\"_index\":90,\"title\":{},\"description\":{\"46\":{}}}],[\"docs/{docid}/tables/{tableid}/record\",{\"_index\":62,\"title\":{},\"description\":{\"28\":{},\"29\":{},\"30\":{},\"31\":{}}}],[\"docs/{docid}/webhook\",{\"_index\":105,\"title\":{},\"description\":{\"53\":{},\"54\":{}}}],[\"docs/{docid}/webhooks/queu\",{\"_index\":111,\"title\":{},\"description\":{\"57\":{}}}],[\"docs/{docid}/webhooks/{webhookid\",{\"_index\":106,\"title\":{},\"description\":{\"55\":{},\"56\":{}}}],[\"document\",{\"_index\":24,\"title\":{\"8\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"33\":{},\"34\":{},\"35\":{},\"53\":{},\"54\":{},\"59\":{},\"60\":{}},\"description\":{\"7\":{},\"15\":{},\"32\":{},\"47\":{},\"52\":{}}}],[\"document'\",{\"_index\":107,\"title\":{\"57\":{}},\"description\":{}}],[\"download\",{\"_index\":98,\"title\":{\"51\":{}},\"description\":{}}],[\"empti\",{\"_index\":28,\"title\":{\"9\":{},\"16\":{},\"57\":{}},\"description\":{}}],[\"endpoint\",{\"_index\":78,\"title\":{},\"description\":{\"42\":{},\"43\":{},\"44\":{},\"45\":{}}}],[\"enumer\",{\"_index\":12,\"title\":{},\"description\":{\"2\":{}}}],[\"excel\",{\"_index\":48,\"title\":{\"24\":{}},\"description\":{}}],[\"favor\",{\"_index\":79,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"fetch\",{\"_index\":61,\"title\":{\"28\":{},\"43\":{}},\"description\":{}}],[\"file\",{\"_index\":46,\"title\":{\"23\":{},\"24\":{},\"25\":{}},\"description\":{\"47\":{}}}],[\"follow\",{\"_index\":54,\"title\":{},\"description\":{\"26\":{}}}],[\"format\",{\"_index\":76,\"title\":{},\"description\":{\"42\":{}}}],[\"frictionlessdata'\",{\"_index\":55,\"title\":{},\"description\":{\"26\":{}}}],[\"grist\",{\"_index\":35,\"title\":{},\"description\":{\"15\":{}}}],[\"group\",{\"_index\":23,\"title\":{},\"description\":{\"7\":{}}}],[\"immedi\",{\"_index\":80,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"includ\",{\"_index\":92,\"title\":{},\"description\":{\"47\":{}}}],[\"list\",{\"_index\":10,\"title\":{\"2\":{},\"5\":{},\"8\":{},\"13\":{},\"21\":{},\"33\":{},\"37\":{},\"48\":{}},\"description\":{}}],[\"metadata\",{\"_index\":38,\"title\":{\"18\":{},\"48\":{},\"50\":{}},\"description\":{}}],[\"modifi\",{\"_index\":17,\"title\":{\"4\":{},\"11\":{},\"18\":{},\"30\":{},\"35\":{},\"39\":{},\"45\":{},\"55\":{}},\"description\":{}}],[\"move\",{\"_index\":40,\"title\":{\"20\":{}},\"description\":{}}],[\"new\",{\"_index\":87,\"title\":{\"54\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"now\",{\"_index\":73,\"title\":{},\"description\":{\"42\":{}}}],[\"option\",{\"_index\":117,\"title\":{\"60\":{}},\"description\":{}}],[\"org\",{\"_index\":3,\"title\":{\"1\":{},\"2\":{},\"3\":{},\"4\":{},\"5\":{},\"6\":{},\"8\":{},\"20\":{}},\"description\":{\"1\":{},\"2\":{}}}],[\"organ\",{\"_index\":22,\"title\":{},\"description\":{\"7\":{}}}],[\"orgs/{orgid\",{\"_index\":16,\"title\":{},\"description\":{\"3\":{},\"4\":{}}}],[\"orgs/{orgid}/access\",{\"_index\":19,\"title\":{},\"description\":{\"5\":{},\"6\":{}}}],[\"orgs/{orgid}/workspac\",{\"_index\":26,\"title\":{},\"description\":{\"8\":{},\"9\":{}}}],[\"paramet\",{\"_index\":118,\"title\":{\"60\":{}},\"description\":{}}],[\"payload\",{\"_index\":110,\"title\":{\"57\":{}},\"description\":{}}],[\"person\",{\"_index\":6,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"plan\",{\"_index\":81,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"point\",{\"_index\":86,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"project\",{\"_index\":88,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"queri\",{\"_index\":114,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"queue\",{\"_index\":108,\"title\":{\"57\":{}},\"description\":{}}],[\"recommend\",{\"_index\":77,\"title\":{},\"description\":{\"42\":{}}}],[\"record\",{\"_index\":59,\"title\":{\"27\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{}},\"description\":{\"27\":{},\"42\":{},\"43\":{},\"44\":{},\"45\":{},\"47\":{}}}],[\"refer\",{\"_index\":93,\"title\":{},\"description\":{\"47\":{}}}],[\"remov\",{\"_index\":82,\"title\":{\"56\":{}},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"request\",{\"_index\":102,\"title\":{},\"description\":{\"52\":{}}}],[\"row\",{\"_index\":60,\"title\":{\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{}}}],[\"run\",{\"_index\":113,\"title\":{\"59\":{},\"60\":{}},\"description\":{}}],[\"same\",{\"_index\":42,\"title\":{\"20\":{}},\"description\":{}}],[\"schema\",{\"_index\":53,\"title\":{\"26\":{}},\"description\":{\"26\":{}}}],[\"securitydefinit\",{\"_index\":1,\"title\":{},\"description\":{\"0\":{}}}],[\"site\",{\"_index\":5,\"title\":{},\"description\":{\"1\":{},\"2\":{},\"7\":{}}}],[\"space\",{\"_index\":7,\"title\":{},\"description\":{\"1\":{}}}],[\"sql\",{\"_index\":112,\"title\":{\"58\":{},\"59\":{},\"60\":{}},\"description\":{}}],[\"sqlite\",{\"_index\":45,\"title\":{\"23\":{}},\"description\":{}}],[\"standard](https://specs.frictionlessdata.io/table-schema\",{\"_index\":57,\"title\":{},\"description\":{\"26\":{}}}],[\"start\",{\"_index\":85,\"title\":{},\"description\":{\"43\":{},\"44\":{},\"45\":{}}}],[\"structur\",{\"_index\":65,\"title\":{},\"description\":{\"32\":{},\"36\":{}}}],[\"tabl\",{\"_index\":50,\"title\":{\"25\":{},\"26\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{},\"37\":{},\"38\":{},\"39\":{},\"40\":{},\"41\":{},\"43\":{},\"44\":{},\"45\":{},\"46\":{}},\"description\":{\"27\":{},\"32\":{},\"36\":{},\"42\":{}}}],[\"table-schema\",{\"_index\":56,\"title\":{},\"description\":{\"26\":{}}}],[\"team\",{\"_index\":4,\"title\":{},\"description\":{\"1\":{},\"2\":{}}}],[\"trigger\",{\"_index\":101,\"title\":{},\"description\":{\"52\":{}}}],[\"type\",{\"_index\":94,\"title\":{},\"description\":{\"47\":{}}}],[\"undeliv\",{\"_index\":109,\"title\":{\"57\":{}},\"description\":{}}],[\"updat\",{\"_index\":64,\"title\":{\"31\":{},\"40\":{}},\"description\":{}}],[\"upload\",{\"_index\":96,\"title\":{\"49\":{}},\"description\":{}}],[\"url\",{\"_index\":103,\"title\":{},\"description\":{\"52\":{}}}],[\"us\",{\"_index\":72,\"title\":{},\"description\":{\"42\":{},\"47\":{}}}],[\"user\",{\"_index\":18,\"title\":{\"5\":{},\"13\":{},\"21\":{}},\"description\":{}}],[\"webhook\",{\"_index\":100,\"title\":{\"52\":{},\"53\":{},\"54\":{},\"55\":{},\"56\":{}},\"description\":{\"52\":{}}}],[\"within\",{\"_index\":25,\"title\":{\"8\":{}},\"description\":{}}],[\"work\",{\"_index\":71,\"title\":{},\"description\":{\"42\":{}}}],[\"workspac\",{\"_index\":21,\"title\":{\"7\":{},\"8\":{},\"9\":{},\"10\":{},\"11\":{},\"12\":{},\"13\":{},\"14\":{},\"20\":{}},\"description\":{\"7\":{},\"15\":{}}}],[\"workspaces/{workspaceid\",{\"_index\":29,\"title\":{},\"description\":{\"10\":{},\"11\":{},\"12\":{}}}],[\"workspaces/{workspaceid}/access\",{\"_index\":31,\"title\":{},\"description\":{\"13\":{},\"14\":{}}}],[\"workspaces/{workspaceid}/doc\",{\"_index\":36,\"title\":{},\"description\":{\"16\":{}}}]],\"pipeline\":[]}},\"options\":{\"theme\":{\"spacing\":{\"sectionVertical\":2},\"breakpoints\":{\"medium\":\"50rem\",\"large\":\"50rem\"},\"sidebar\":{\"width\":\"0px\"}},\"hideDownloadButton\":true,\"pathInMiddlePanel\":true,\"scrollYOffset\":48,\"jsonSampleExpandLevel\":\"all\"}}; var container = document.getElementById('redoc'); Redoc.hydrate(__redoc_state, container);","title":"REST API reference"},{"location":"api/#grist-api-reference","text":"REST API for manipulating documents, workspaces, and team sites. API Usage is an introduction to using the API. API Console allows you to make API calls from the browser. Authentication orgs get List the orgs you have access to get Describe an org patch Modify an org get List users with access to org patch Change who has access to org workspaces get List workspaces and documents within an org post Create an empty workspace get Describe a workspace patch Modify a workspace del Delete a workspace get List users with access to workspace patch Change who has access to workspace docs post Create an empty document get Describe a document patch Modify document metadata (but not its contents) del Delete a document patch Move document to another workspace in the same org. get List users with access to document patch Change who has access to document get Content of document, as an Sqlite file get Content of document, as an Excel file get Content of table, as a CSV file get The schema of a table records get Fetch records from a table post Add records to a table patch Modify records of a table put Add or update records of a table tables get List tables in a document post Add tables to a document patch Modify tables of a document columns get List columns in a table post Add columns to a table patch Modify columns of a table put Add or update columns of a table del Delete a column of a table data get Fetch data from a table post Add rows to a table patch Modify rows of a table post Delete rows of a table attachments get List metadata of all attachments in a doc post Upload attachments to a doc get Get the metadata for an attachment get Download the contents of an attachment webhooks get Webhooks associated with a document post Create new webhooks for a document patch Modify a webhook del Remove a webhook del Empty a document's queue of undelivered payloads sql get Run an SQL query against a document post Run an SQL query against a document, with options or parameters API docs by Redocly","title":"Grist API Reference"},{"location":"authorship/","text":"Authorship columns # Sometimes it is useful to have columns that record who created individual records, and who last updated them. Grist lets you create such columns easily. It also automatically tracks document changes in the Activity tab of Document History, but nevertheless it is convenient to have that information in tabular form available to formulas and filters, and authorship columns let you do that. A \u201cCreated By\u201d column # Suppose we want to fill a column automatically with the name of the creator of each record as they are added. As a first step, add a column called (for example) Created By . In the column options in the side panel (see Columns for a refresher), click Set trigger formula action. Set user.Name as the column\u2019s formula. There are other possibilities, such as user.Email , a unique user.UserID , and so on. The user information available is the same as that in Access rule conditions . Time information is available as well (see Timestamp columns ). But let\u2019s stick with user.Name for now. Now, to set the column whenever a record is created, make sure that Apply to new records option is checked. And that\u2019s it! Now whenever a record is created, the Created At column will be set to the name of the user creating it: An \u201cUpdated By\u201d column # If we want a column that stores who last edited a record (as opposed to its creator), the procedure is similar to that for a \u201cCreated By\u201d column , but instead of Apply to new records , select Apply on record changes . Then select Any field (assuming you want any change in a record to count as an update) and press OK . You can alternatively pick and choose which columns, when updated, will trigger the formula. Here is an example the the new column at work - when Cotton Candy v Candy Floss is updated,a user name appears for that record: It is still possible for a user to manually edit cells in the Created By and Updated By columns. If you don\u2019t want that to be allowed, use access rules to forbid it.","title":"Authorship columns"},{"location":"authorship/#authorship-columns","text":"Sometimes it is useful to have columns that record who created individual records, and who last updated them. Grist lets you create such columns easily. It also automatically tracks document changes in the Activity tab of Document History, but nevertheless it is convenient to have that information in tabular form available to formulas and filters, and authorship columns let you do that.","title":"Authorship columns"},{"location":"authorship/#a-created-by-column","text":"Suppose we want to fill a column automatically with the name of the creator of each record as they are added. As a first step, add a column called (for example) Created By . In the column options in the side panel (see Columns for a refresher), click Set trigger formula action. Set user.Name as the column\u2019s formula. There are other possibilities, such as user.Email , a unique user.UserID , and so on. The user information available is the same as that in Access rule conditions . Time information is available as well (see Timestamp columns ). But let\u2019s stick with user.Name for now. Now, to set the column whenever a record is created, make sure that Apply to new records option is checked. And that\u2019s it! Now whenever a record is created, the Created At column will be set to the name of the user creating it:","title":"A \"Created By\" column"},{"location":"authorship/#an-updated-by-column","text":"If we want a column that stores who last edited a record (as opposed to its creator), the procedure is similar to that for a \u201cCreated By\u201d column , but instead of Apply to new records , select Apply on record changes . Then select Any field (assuming you want any change in a record to count as an update) and press OK . You can alternatively pick and choose which columns, when updated, will trigger the formula. Here is an example the the new column at work - when Cotton Candy v Candy Floss is updated,a user name appears for that record: It is still possible for a user to manually edit cells in the Created By and Updated By columns. If you don\u2019t want that to be allowed, use access rules to forbid it.","title":"An \"Updated By\" column"},{"location":"automatic-backups/","text":"Automatic Backups # Grist automatically saves backups of your documents as you work on them. These backups \u2013 or snapshots \u2013 of the document can be examined at any time. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. For example, hourly snapshots are retained for about a day, but monthly snapshots are retained for more than a year. Examining Backups # To see the list of backups, click \u201cDocument History\u201d in the left panel, then click the \u201cSnapshots\u201d tab in the panel that opens on the right: You can open any of the listed snapshots to see the document as it existed at that time. Restoring an Older Version # While examining a snapshot, the \u201cShare\u201d menu has additional options; Use the \u201cReplace Current Version\u201d option to revert your document to the version you are looking at. You can also save the snapshot as a new document using the \u201cSave Copy\u201d option. Deleted Documents # When you delete a document, its history of backups is deleted with it. While the document is in Trash, you can still restore it for 30 days. If you do, you will find all the historical snapshots still there. If you choose \u201cDelete Forever\u201d for a document in Trash, or when a document in Trash is automatically purged after 30 days, the backups will be deleted forever with it.","title":"Automatic backups"},{"location":"automatic-backups/#automatic-backups","text":"Grist automatically saves backups of your documents as you work on them. These backups \u2013 or snapshots \u2013 of the document can be examined at any time. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. For example, hourly snapshots are retained for about a day, but monthly snapshots are retained for more than a year.","title":"Automatic Backups"},{"location":"automatic-backups/#examining-backups","text":"To see the list of backups, click \u201cDocument History\u201d in the left panel, then click the \u201cSnapshots\u201d tab in the panel that opens on the right: You can open any of the listed snapshots to see the document as it existed at that time.","title":"Examining Backups"},{"location":"automatic-backups/#restoring-an-older-version","text":"While examining a snapshot, the \u201cShare\u201d menu has additional options; Use the \u201cReplace Current Version\u201d option to revert your document to the version you are looking at. You can also save the snapshot as a new document using the \u201cSave Copy\u201d option.","title":"Restoring an Older Version"},{"location":"automatic-backups/#deleted-documents","text":"When you delete a document, its history of backups is deleted with it. While the document is in Trash, you can still restore it for 30 days. If you do, you will find all the historical snapshots still there. If you choose \u201cDelete Forever\u201d for a document in Trash, or when a document in Trash is automatically purged after 30 days, the backups will be deleted forever with it.","title":"Deleted Documents"},{"location":"browser-support/","text":"Browser Support # Grist is officially supported and regularly tested on modern Firefox and Google Chrome browsers on all desktop platforms. These are available here: Get Firefox Get Chrome Other modern browsers will work to the degree they are standards compliant. In particular, Grist is reported to work on modern Safari and Microsoft Edge. If you encounter errors or unexpected behavior on these browsers, we encourage you to report them to us by emailing us at support@getgrist.com . Mobile Support # You can use Grist quite comfortably in mobile browsers such as Chrome, Safari, and Firefox. On small screens, you can find the list of pages from the button in the bottom bar. For pages with multiple widgets, only the active widget is expanded. Other widgets are collapsed until you touch to expand them. To edit a cell, simply double-tap it. Mobile support is still a work in progress. To get back to the desktop version on a mobile device, open your account menu, and select \u201cToggle Mobile Mode\u201d option:","title":"Browser Support"},{"location":"browser-support/#browser-support","text":"Grist is officially supported and regularly tested on modern Firefox and Google Chrome browsers on all desktop platforms. These are available here: Get Firefox Get Chrome Other modern browsers will work to the degree they are standards compliant. In particular, Grist is reported to work on modern Safari and Microsoft Edge. If you encounter errors or unexpected behavior on these browsers, we encourage you to report them to us by emailing us at support@getgrist.com .","title":"Browser Support"},{"location":"browser-support/#mobile-support","text":"You can use Grist quite comfortably in mobile browsers such as Chrome, Safari, and Firefox. On small screens, you can find the list of pages from the button in the bottom bar. For pages with multiple widgets, only the active widget is expanded. Other widgets are collapsed until you touch to expand them. To edit a cell, simply double-tap it. Mobile support is still a work in progress. To get back to the desktop version on a mobile device, open your account menu, and select \u201cToggle Mobile Mode\u201d option:","title":"Mobile Support"},{"location":"col-refs/","text":"Reference and Reference Lists # Overview # Reference and Reference List columns in Grist allow one table to create an explicit reference to another. In the database world this is similar to a foreign key. In the spreadsheet world this is similar to a VLOOKUP , but much more powerful and easier to use. In this guide we\u2019ll use the term underlying table for the table that lists all available values, and referencing table for the table that uses those values. Creating a new Reference column # Suppose we have a document with two tables, Clients and Projects. The Clients table lists our clients - names, contacts, signing dates - and the Projects table lists projects we do for clients. There are all sorts of things Grist can do for us if we let it know that the Client column in the Projects table is referring to clients listed in the Clients table. We can do this by converting the Client column to a \u201creference column\u201d. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d to \u201cReference\u201d. Adjust the \u201cData from Table\u201d option to be the correct table you want to cross-reference, and the \u201cShow Column\u201d option to match which column of that table you\u2019d like to show. Then hit \u201cApply\u201d when you\u2019re happy with the result. Understanding the reference The column value always references the entire record in the underlying table. The displayed value can be any column from that record, as selected in Show Column . You can also include additional columns to display as explained later. In our example, you can see little link icons appearing in the Client column cells, showing that they have been successfully cross-referenced with the Clients table. Once the column type is set, you can start typing into it or double-click it to see a dropdown list of all available values. Note that the table Clients and the column Client are related by the column type rather than by name. They can be named anything. Spotting reference columns You can tell that the values in a column represent a reference by the link icon that appears next to the values. If you accidentally type in a value that is not present in the Clients table, its value will be highlighted as invalid: Adding values to a Reference column # Sometimes it\u2019s useful to add a new value to the dropdown list without having to switch to the underlying table. Reference columns make it easy! Just type in the value you want add and select the + value in the dropdown list. Grist will automatically add a new record containing this value to the underlying table and insert the proper reference: Converting Text column to Reference # When working with existing data, it\u2019s common to have existing text values that should really be reference values. Don\u2019t worry, conversion is simple! Just change the column type to Reference and Grist will automatically find and substitute matching values for references. If some values are not found, they\u2019ll be shown as invalid. You can then either add them to the underlying table or select the proper values for them. In this example, the first three values match perfectly, but Forest Labs is invalid because it doesn\u2019t exist in the Clients table: Including multiple fields from a reference # A big benefit of reference columns is that they allow you to easily bring in multiple columns from the underlying table. In our example, if you wanted to bring in $Client.Contact to the Projects table, you can just select the Contact column from the Add Referenced Columns section and it will be automatically added to the Projects table: If you\u2019re comfortable using formulas, you can see that the added column is just the formula =$Client.Contact . If you were so inclined, you could achieve the same result by manually adding the formula column. You can also use any other fields from Clients table by referencing $Client in formulas in the Projects table. Note that in formulas, we use the name of the reference column ( $Client ) to refer to a linked record, not the name of the table (which is Clients here). Don\u2019t let the similarity of the names in this example confuse you. Using references in formulas You may have noticed that the underlying table is Clients (plural) but the formula is $Client.Contact (singular). That\u2019s because the formula refers to the referencing column, not the underlying table. In our example, the referencing column is Client . Creating a new Reference List column # So far our example has only dealt with projects that have a single client. Suppose that we also have projects with multiple clients, and we\u2019d like to maintain references to them all from the Client column of the Projects table. We can let Grist know that the Client column contains multiple references by changing its type to \u201cReference List\u201d. This column type can reference multiple records, and can also be thought of as a multi-select. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d of Client to \u201cReference List\u201d. Grist will automatically convert any of your existing references to reference lists. Once you\u2019re happy with the result, just hit \u201cApply\u201d and the Client column will be ready to accept as many clients as your projects need. Editing values in a Reference List column # To make changes to a Reference List cell, simply double-click the cell or press the Enter key after you have selected the cell you want to edit. You can also start typing after selecting a cell if you\u2019d like to write over any existing contents. Doing so will open an editor like the one in the example below. Like with Reference columns, the autocomplete menu will populate with suggestions as you type. If you type in a value that\u2019s not present in the referenced table, you can select the + value to add a new row to the referenced table with your value. To delete existing references, simply press the Backspace key, or move your cursor over a reference and click the X icon. You can also rearrange references in the editor by dragging them with your mouse. To save your changes and close the editor, either press Enter or Tab , or click anywhere outside the editor. To close the editor and discard any changes you\u2019ve made, press Escape . Understanding reference columns # Cells in a reference column always identify an entire record in the referenced table. For convenience, you may select which column from that record to show by setting the \u201cSHOW COLUMN\u201d. However, the cell\u2019s value is always a record\u2019s unique ID. Similarly, Reference Lists store a list of record ids. What does that really mean? Let\u2019s take a look at the Class Enrollment template. In the Classes table, the Instructor column is a reference column that references data from the Staff table. Full Name is selected under \u2018Show Column\u2019 and is used as a label to represent the record from the Staff table that is being referenced here. We can change that label to any other value contained within the record. Let\u2019s change it to \u2018Row ID\u2019. The row ID is what is actually being stored within the Reference or Reference List column. With this ID, we can fetch any data associated with this record. In the first row of the Classes table, we see Staff[2] as the value in the Instructor column. This represents the record in the Staff table with Row ID = 2 . We can navigate to the Staff table and see which record is assigned Row ID = 2 . To view a record\u2019s unique ID, add a new column with the formula = $id . We can see that the value in the Full Name column for the record with Row ID = 2 is Dowbakin, Daniella . If we revert back to our original settings for the Instructor column of the Classes table, where Full Name was selected under Show Column, we see that the Full Name value associated with Staff[2] is Dowbakin, Daniella . Filtering Reference choices in dropdown # When entering data into a reference column you will see a dropdown list of all available values to choose from. Sometimes the list can get long, and in some cases confusing. For example, say you\u2019re tracking population changes in the 1,000 most populous world cities. When entering a city into the reference column for city selection, the dropdown lists all 1,000 cities. It would be useful if the dropdown list of city choices were filtered based on the country selected in the Country column. To filter a reference column\u2019s dropdown list, select the reference column then set a \u201cDropdown Condition\u201c in the Creator Panel under the \u201cColumn\u201d tab. You can filter a dropdown\u2019s choice by writing a condition as a formula. The attribute choice refers to choices in the dropdown. In this case the formula is choice.Country == $Country . Why did that work? The city column is a reference column pointing to a Cities table that matches countries and cities. That table looks like this. The formula condition choice.Country == $Country is looking up each choice\u2019s country in the Cities table using a reference lookup , then it compares those countries to the value entered in the Country column of the Population Rankings table. The dropdown now lists only choices (aka cities) whose country equals the country entered in Country column. The choice attribute can also be used when setting dropdown filter conditions for Choice and Choice List columns. Note that because reference dropdown filtering is written as formulas, filtering can be very flexible and granular. Users experienced with access rules may notice similarities in how to think about writing these formulas.","title":"Reference columns"},{"location":"col-refs/#reference-and-reference-lists","text":"","title":"Reference and Reference Lists"},{"location":"col-refs/#overview","text":"Reference and Reference List columns in Grist allow one table to create an explicit reference to another. In the database world this is similar to a foreign key. In the spreadsheet world this is similar to a VLOOKUP , but much more powerful and easier to use. In this guide we\u2019ll use the term underlying table for the table that lists all available values, and referencing table for the table that uses those values.","title":"Overview"},{"location":"col-refs/#creating-a-new-reference-column","text":"Suppose we have a document with two tables, Clients and Projects. The Clients table lists our clients - names, contacts, signing dates - and the Projects table lists projects we do for clients. There are all sorts of things Grist can do for us if we let it know that the Client column in the Projects table is referring to clients listed in the Clients table. We can do this by converting the Client column to a \u201creference column\u201d. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d to \u201cReference\u201d. Adjust the \u201cData from Table\u201d option to be the correct table you want to cross-reference, and the \u201cShow Column\u201d option to match which column of that table you\u2019d like to show. Then hit \u201cApply\u201d when you\u2019re happy with the result. Understanding the reference The column value always references the entire record in the underlying table. The displayed value can be any column from that record, as selected in Show Column . You can also include additional columns to display as explained later. In our example, you can see little link icons appearing in the Client column cells, showing that they have been successfully cross-referenced with the Clients table. Once the column type is set, you can start typing into it or double-click it to see a dropdown list of all available values. Note that the table Clients and the column Client are related by the column type rather than by name. They can be named anything. Spotting reference columns You can tell that the values in a column represent a reference by the link icon that appears next to the values. If you accidentally type in a value that is not present in the Clients table, its value will be highlighted as invalid:","title":"Creating a new Reference column"},{"location":"col-refs/#adding-values-to-a-reference-column","text":"Sometimes it\u2019s useful to add a new value to the dropdown list without having to switch to the underlying table. Reference columns make it easy! Just type in the value you want add and select the + value in the dropdown list. Grist will automatically add a new record containing this value to the underlying table and insert the proper reference:","title":"Adding values to a Reference column"},{"location":"col-refs/#converting-text-column-to-reference","text":"When working with existing data, it\u2019s common to have existing text values that should really be reference values. Don\u2019t worry, conversion is simple! Just change the column type to Reference and Grist will automatically find and substitute matching values for references. If some values are not found, they\u2019ll be shown as invalid. You can then either add them to the underlying table or select the proper values for them. In this example, the first three values match perfectly, but Forest Labs is invalid because it doesn\u2019t exist in the Clients table:","title":"Converting Text column to Reference"},{"location":"col-refs/#including-multiple-fields-from-a-reference","text":"A big benefit of reference columns is that they allow you to easily bring in multiple columns from the underlying table. In our example, if you wanted to bring in $Client.Contact to the Projects table, you can just select the Contact column from the Add Referenced Columns section and it will be automatically added to the Projects table: If you\u2019re comfortable using formulas, you can see that the added column is just the formula =$Client.Contact . If you were so inclined, you could achieve the same result by manually adding the formula column. You can also use any other fields from Clients table by referencing $Client in formulas in the Projects table. Note that in formulas, we use the name of the reference column ( $Client ) to refer to a linked record, not the name of the table (which is Clients here). Don\u2019t let the similarity of the names in this example confuse you. Using references in formulas You may have noticed that the underlying table is Clients (plural) but the formula is $Client.Contact (singular). That\u2019s because the formula refers to the referencing column, not the underlying table. In our example, the referencing column is Client .","title":"Including multiple fields from a reference"},{"location":"col-refs/#creating-a-new-reference-list-column","text":"So far our example has only dealt with projects that have a single client. Suppose that we also have projects with multiple clients, and we\u2019d like to maintain references to them all from the Client column of the Projects table. We can let Grist know that the Client column contains multiple references by changing its type to \u201cReference List\u201d. This column type can reference multiple records, and can also be thought of as a multi-select. Open the Column Options side panel (see Specifying a type ) and set the \u201cColumn Type\u201d of Client to \u201cReference List\u201d. Grist will automatically convert any of your existing references to reference lists. Once you\u2019re happy with the result, just hit \u201cApply\u201d and the Client column will be ready to accept as many clients as your projects need.","title":"Creating a new Reference List column"},{"location":"col-refs/#editing-values-in-a-reference-list-column","text":"To make changes to a Reference List cell, simply double-click the cell or press the Enter key after you have selected the cell you want to edit. You can also start typing after selecting a cell if you\u2019d like to write over any existing contents. Doing so will open an editor like the one in the example below. Like with Reference columns, the autocomplete menu will populate with suggestions as you type. If you type in a value that\u2019s not present in the referenced table, you can select the + value to add a new row to the referenced table with your value. To delete existing references, simply press the Backspace key, or move your cursor over a reference and click the X icon. You can also rearrange references in the editor by dragging them with your mouse. To save your changes and close the editor, either press Enter or Tab , or click anywhere outside the editor. To close the editor and discard any changes you\u2019ve made, press Escape .","title":"Editing values in a Reference List column"},{"location":"col-refs/#understanding-reference-columns","text":"Cells in a reference column always identify an entire record in the referenced table. For convenience, you may select which column from that record to show by setting the \u201cSHOW COLUMN\u201d. However, the cell\u2019s value is always a record\u2019s unique ID. Similarly, Reference Lists store a list of record ids. What does that really mean? Let\u2019s take a look at the Class Enrollment template. In the Classes table, the Instructor column is a reference column that references data from the Staff table. Full Name is selected under \u2018Show Column\u2019 and is used as a label to represent the record from the Staff table that is being referenced here. We can change that label to any other value contained within the record. Let\u2019s change it to \u2018Row ID\u2019. The row ID is what is actually being stored within the Reference or Reference List column. With this ID, we can fetch any data associated with this record. In the first row of the Classes table, we see Staff[2] as the value in the Instructor column. This represents the record in the Staff table with Row ID = 2 . We can navigate to the Staff table and see which record is assigned Row ID = 2 . To view a record\u2019s unique ID, add a new column with the formula = $id . We can see that the value in the Full Name column for the record with Row ID = 2 is Dowbakin, Daniella . If we revert back to our original settings for the Instructor column of the Classes table, where Full Name was selected under Show Column, we see that the Full Name value associated with Staff[2] is Dowbakin, Daniella .","title":"Understanding reference columns"},{"location":"col-refs/#filtering-reference-choices-in-dropdown","text":"When entering data into a reference column you will see a dropdown list of all available values to choose from. Sometimes the list can get long, and in some cases confusing. For example, say you\u2019re tracking population changes in the 1,000 most populous world cities. When entering a city into the reference column for city selection, the dropdown lists all 1,000 cities. It would be useful if the dropdown list of city choices were filtered based on the country selected in the Country column. To filter a reference column\u2019s dropdown list, select the reference column then set a \u201cDropdown Condition\u201c in the Creator Panel under the \u201cColumn\u201d tab. You can filter a dropdown\u2019s choice by writing a condition as a formula. The attribute choice refers to choices in the dropdown. In this case the formula is choice.Country == $Country . Why did that work? The city column is a reference column pointing to a Cities table that matches countries and cities. That table looks like this. The formula condition choice.Country == $Country is looking up each choice\u2019s country in the Cities table using a reference lookup , then it compares those countries to the value entered in the Country column of the Population Rankings table. The dropdown now lists only choices (aka cities) whose country equals the country entered in Country column. The choice attribute can also be used when setting dropdown filter conditions for Choice and Choice List columns. Note that because reference dropdown filtering is written as formulas, filtering can be very flexible and granular. Users experienced with access rules may notice similarities in how to think about writing these formulas.","title":"Filtering Reference choices in dropdown"},{"location":"col-transform/","text":"Column Transformations # Grist offers two ways to transform all values in a column. One is to change the type of the column, and the other is to apply a formula-based transformation. Type conversions # When converting between different column types, Grist has sensible default behavior, but makes that behavior easy to revise. For example, suppose you have a column of integers. To convert that column to text, open the column options as described in Specifying a type , and find the column type section. Change the column type to text in the dropdown. You\u2019ll notice that a \u201ccancel/revise/apply\u201d dialog opens beside the dropdown. To change how the conversion is done, click Revise . You\u2019ll see a formula box with the default conversion method, grist.Text.typeConvert($tally) . This means \u201cdo default conversion to text for the tally column\u201d. you can replace this with any formula you like. For example: Code for converting to unicode tally lines is left as an exercise to the reader. To preview the results of the conversion, click \u201cpreview\u201d. When you are satisfied with the conversion, click \u201capply\u201d. To abandon the conversion, click \u201ccancel\u201d. Formula-based transforms # Spreadsheets are convenient tools for cleaning up data using formulas . For example, imagine you had zip codes that have lost leading zeros - you can easily reformat them with a quick formula: We could now freeze the results and delete the original data if we don\u2019t need it anymore. If you know you\u2019re going to throw away the original data like this, Grist offers column transformations as a faster way to systematically modify all cells of a column. Find the \u201cTransform\u201d section at the bottom of the column options side panel (see Columns for how to open this panel). When you click the orange \u201clightning\u201d button, Grist prompts you with a formula, return $zip in this case. You can edit this formula to make some change to the selected column. For example return $zip + 1 would add one to the zip code. You can preview the effect your formula would have, and when you are happy, hit \u201cApply\u201d. In our case, where we want to add leading zeros, we\u2019ll need to first change our column type to be Text (assuming it is currently Integer - if it Numeric convert to Integer first and then to Text to avoid decimal points). Once done, we can use our formula for adding leading zeros: When happy, press \u201cApply\u201d to replace the cell values with their new versions. Likewise, the response column could be transformed with the formula into true/false values with $response[0] == 'y' , and then set as a toggle column .","title":"Transformations"},{"location":"col-transform/#column-transformations","text":"Grist offers two ways to transform all values in a column. One is to change the type of the column, and the other is to apply a formula-based transformation.","title":""},{"location":"col-transform/#type-conversions","text":"When converting between different column types, Grist has sensible default behavior, but makes that behavior easy to revise. For example, suppose you have a column of integers. To convert that column to text, open the column options as described in Specifying a type , and find the column type section. Change the column type to text in the dropdown. You\u2019ll notice that a \u201ccancel/revise/apply\u201d dialog opens beside the dropdown. To change how the conversion is done, click Revise . You\u2019ll see a formula box with the default conversion method, grist.Text.typeConvert($tally) . This means \u201cdo default conversion to text for the tally column\u201d. you can replace this with any formula you like. For example: Code for converting to unicode tally lines is left as an exercise to the reader. To preview the results of the conversion, click \u201cpreview\u201d. When you are satisfied with the conversion, click \u201capply\u201d. To abandon the conversion, click \u201ccancel\u201d.","title":"Type conversions"},{"location":"col-transform/#formula-based-transforms","text":"Spreadsheets are convenient tools for cleaning up data using formulas . For example, imagine you had zip codes that have lost leading zeros - you can easily reformat them with a quick formula: We could now freeze the results and delete the original data if we don\u2019t need it anymore. If you know you\u2019re going to throw away the original data like this, Grist offers column transformations as a faster way to systematically modify all cells of a column. Find the \u201cTransform\u201d section at the bottom of the column options side panel (see Columns for how to open this panel). When you click the orange \u201clightning\u201d button, Grist prompts you with a formula, return $zip in this case. You can edit this formula to make some change to the selected column. For example return $zip + 1 would add one to the zip code. You can preview the effect your formula would have, and when you are happy, hit \u201cApply\u201d. In our case, where we want to add leading zeros, we\u2019ll need to first change our column type to be Text (assuming it is currently Integer - if it Numeric convert to Integer first and then to Text to avoid decimal points). Once done, we can use our formula for adding leading zeros: When happy, press \u201cApply\u201d to replace the cell values with their new versions. Likewise, the response column could be transformed with the formula into true/false values with $response[0] == 'y' , and then set as a toggle column .","title":"Formula-based transforms"},{"location":"col-types/","text":"Columns and data types # Adding and removing columns # Every Grist table, when first created, has three columns called A, B, and C. To rename a column, hover on the column header, click on the drop-down, then select \u201cRename column\u201d (you can also just click on the column header twice). To delete a column, hover on the column header, click on the drop-down, then select \u201cDelete column\u201d. To add a column, click on the \u201c+\u201d symbol in the header row to open the \u201cAdd Column\u201d menu. The first option in the menu, \u201cAdd Column\u201d, will add a new, empty data column to your table. If you know what column type you need, the next option allows you to assign a type for your new column. Select a column type from the expanded menu. The third option, \u201cAdd formula column\u201d, will add a new formula column with the formula entry box immediately opened so you can begin entering your formula without additional clicks. \u201cHidden Columns\u201d expands to show a list of columns hidden from this view that can be quickly added back. \u201cLookups\u201d allows you to add data columns from related tables. You can use reference columns to relate data in different tables. Learn more about References and Lookups . \u201cShortcuts\u201d lists the most frequently used trigger formula functions. Learn more about each shortcut option at the links below: Timestamp Authorship Detect Duplicates in\u2026 UUID Reordering columns # To reorder a column, first select the column if it isn\u2019t already selected, by clicking on the column header. Next, click and hold on the column header. After a second or two, you\u2019ll be able to drag the entire column to its new location. Another way to reorder columns is via the widget options: In the visible columns section, the columns can be dragged around freely to reorder them. You can also hide columns here. Renaming columns # You can rename columns in several ways. One way is to double click a column header. Then, you can rename the column or add a column description. You can also hover on the column header, click on the drop-down, then select \u201cRename Column\u201d. This opens the same pop up seen above. Selecting \u201cColumn Options\u201d in the same drop-down opens the creator panel. From here, you can edit the Column Label, shown at the top or add a description. A bonus with this method is that you can also control the identifier given to the column in formulas. By default this is based on the field name, with any characters Python doesn\u2019t like replaced with \u201c_\u201c, and a number added if needed to keep the name unique within your table. If you don\u2019t like this identifier, you can change it, though it will still need to be Python-friendly. Click the link icon to make the ID field editable, then enter the new Column ID. Formatting columns # Header and cell styles can be modified under the Column tab of the creator panel. When you open the styling menu, you have the option to apply text formatting as well as text and fill colors. If you want more color options, click the fill color box then find the shade you want. If you have a specific color you wish to use, you can enter the hex code, RGB or HSL values. Cell style can also be changed based on conditional rules. Learn more about Conditional Formatting . Specifying a type # Grist columns have types, similar to other spreadsheets or databases. The type of a column controls its appearance and the help Grist will offer you when editing cells. When you create a new column, it initially has the Any type. When you enter the column\u2019s first cell, Grist tries to narrow this type. If you enter a number, the column will be changed to Numeric type, which is right-aligned by default. If you enter something that doesn\u2019t look like a number, the column will be changed to Text type, which is left-aligned by default. To inspect the type of a column, hover over the column header, then click on the drop-down, then select \u201cColumn Options\u201d. The \u201cColumn Type\u201d section is what you are looking for. You will often want to control the column type manually. You can change it in the \u201cColumn Type\u201d section. For example, here we set a column full of \u201cyes\u201d and \u201cno\u201d responses to be of type Toggle : One advantage of doing so is that Grist can now offer you ways to visualize the column that are specialized to on/off style values. Each column type has different options in the \u201cCell Format\u201d section of the side panel: Regardless of the column type, you can enter any value in cells. If a value entered is incompatible with the defined type, the cell will be highlighted with an error (and columns referencing the invalid value will also display an error): Supported types # Grist supports the following types: Type Description Text ( Default ) Any string of text. Numeric Floating point numbers. Integer Integers (whole numbers). Toggle Boolean (True / False) Date Valid date (without a time component). DateTime Valid date + time. Choice Single value from a list of pre-defined valid values. Choice List Multiple values from a list of pre-defined valid values. Reference A reference column to another table. Reference List A list of references to another table. Attachment Cells where you can place files or images. Text columns # You can put any text you like in this type of column. For formatting, you can control alignment and word-wrap, text color and background color. If the column is used for storing web links, you can turn on \u201cHyperLink\u201d formatting to make links prettier and to include a clickable link icon. Hyperlinks # When a Text column uses \u201cHyperLink\u201d formatting, values get formatted like so: https://getgrist.com will show https://getgrist.com . Grist Labs https://getgrist.com will show Grist Labs (linking to \u201chttps://getgrist.com\u201d with \u201cGrist Labs\u201d as the text). Email Help mailto:support@getgrist.com will show Email Help , a link which would open an email program to compose an email to support@getgrist.com. In general, the value until the last space is used as the link text, while the last word is used as the link destination. Link formatting is particularly useful when links are generated using a formula such as: $Company + \" \" + $Website Numeric columns # This type is for numbers, including floating-point numbers. In addition to controlling alignment and color, you can choose the number format, and the minimum and maximum number of digits to show after the decimal point. Choosing the \u201cSpinner\u201d option for CELL FORMAT will show arrows in each cell for increasing/decreasing the number. The options under NUMBER FORMAT include: $ : Format for currency amounts, such as dollars or euro. Selecting the $ will add a currency prefix, thousands separators, and default to 2 digits after the decimal point. It will also open a currency selector for international currencies. Setting Default Currency You can set a document\u2019s default timezone, locale, and currency in Document Settings . , : Turn on the display of thousands separators. % : Show numbers as percentages. E.g. \u201c0.5\u201d would show as \u201c50%\u201d. Exp : Show numbers in exponential (or scientific) notation. E.g. \u201c1234\u201d would be shown as \u201c1.234E3\u201d. (-) : Show negative numbers in parentheses, without a leading minus sign. This is commonly used in accounting, and usually combined with $ or , formats. Integer columns # This is strictly for whole numbers. It has the same options as the numeric type. Toggle columns # This type is for storing true/false values. The values can be shown as text, checkboxes, or switches. See also example in Specifying a type . Date columns # This type is for storing calendar dates (without a time of day component). More details in Working with dates . You can choose the format for dates, see the date formatting reference . DateTime columns # This type is for storing calendar dates plus time of day. More details in Working with dates . You can choose the format for dates, see the date and time formatting reference . You can also specify the timezone to display for. If you\u2019d like to set a default timezone for your document, you can do so in Document Settings . Choice columns # This type is for storing one of a set of valid values, where you get to specify the available values. There\u2019s an example of using this type of column in the Lightweight CRM example . If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. You can add or remove choices by either clicking Edit or on the Choices box. To add a choice, type its value in the text field below the other choices and press Enter . To remove a choice, click the delete icon to the right of the choice or select the choice by clicking on it, and then press Backspace / Delete . To apply/save your changes, you can either click the Save button or press Enter . To discard your changes, you can either click the Cancel button or press Escape . Clicking the color dropdown to the left of a choice will open a color picker for customizing the fill and text color of a choice. Changes to colors are reflected inside cells and throughout the rest of your document once you save your changes. Choices can be re-arranged by clicking and dragging them, which determines the order in which they appear when typing into a cell. You can also rename a choice by clicking on it and typing a new name. Renaming a choice will also rename all the values that are used in your document. The configuration editor supports many convenient keyboard shortcuts. You can press the Up Arrow and Down Arrow keys to navigate between selected choices; hold the Shift key to bulk select adjacent choices while clicking or using the arrow keys; and hold the Command / Control key to multi-select choices while clicking. To select all choices, you can press Command / Control + A . Undo and redo are also supported in the configuration editor. While your cursor has focus on the editor text field, you can press Command / Control + Z to undo your last change, and Command / Control + Shift + Z to redo it. The configuration editor also supports copy and paste. To copy, select the choices you want copied and press Command / Control + C . To paste, focus on the text field and press Command / Control + V . Choices are pasted in bulk if the clipboard contains multiple lines of text. Choices can also be copied from one column\u2019s configuration editor and pasted into another, which will copy over both the values and their configured colors. When typing into a Choice column cell, your configured choices will be shown in an autocomplete menu. You can either click on a choice, or use the arrow keys and Enter to add a choice to a cell. If your input is not one of the valid choices, Grist will display a menu option for conveniently adding it as a valid choice and into the cell in one step. Choice List columns # This type is for storing multiple values from a set of valid values, where you get to specify the available values. If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. Choice List columns are configured the same way Choice columns are, and support the same level of customization and keyboard shortcuts. They differ in the number of choices they allow to be entered in each cell. While Choice columns only allow at most one value in a cell, Choice List columns allow many. Like with Choice columns, when typing into a Choice List column cell, the valid choices will be shown in an autocomplete menu. Once you\u2019ve selected a value, you can continue adding choices to the same cell. Choices can be re-arranged inside their cells by clicking and dragging them while the cell is being edited. You can also use the arrow keys and the Delete key to navigate and delete choices, or simply click the delete icon when hovering your cursor over a choice. Filtering Choice and Choice List columns\u2019 dropdown lists When entering data into a Choice or Choice List column you will see a dropdown list of all available choices. Sometimes it would be useful to filter the dropdown list based on a condition, such as the value in another cell. Writing conditions to filter choice dropdown lists is similar to filtering reference column\u2019s dropdown lists . Reference columns # This sets up a cross-reference to another table. You can specify the table to reference, and a column within that table to show. There\u2019s a lot you can do with this kind of column, see Reference columns for details. Reference List columns # Like Reference columns , but can store multiple references in a single cell. There\u2019s a lot you can do with this kind of column, see Reference columns for details. Attachment columns # This column type lets you insert entire files and images in cells. When images are added in cells, a preview thumbnail is shown in the cell. The \u201cSize\u201d bar gives control of the scale of this thumbnail. When you create an attachment column, cells of that column will have a paperclip icon: When you click on a paperclip icon, you can select a file to attach. If it is an image, you\u2019ll see a thumbnail of it in the cell. If you hover over the image, you\u2019ll see a paperclip icon again, which you can use to add more files to the same cell. You\u2019ll also see an \u201copen-eye\u201d icon, which when clicked brings up a larger view of all of the cell\u2019s attachments, and gives you a way to rename them, download them, or remove them.","title":"Columns & types"},{"location":"col-types/#columns-and-data-types","text":"","title":"Columns and data types"},{"location":"col-types/#adding-and-removing-columns","text":"Every Grist table, when first created, has three columns called A, B, and C. To rename a column, hover on the column header, click on the drop-down, then select \u201cRename column\u201d (you can also just click on the column header twice). To delete a column, hover on the column header, click on the drop-down, then select \u201cDelete column\u201d. To add a column, click on the \u201c+\u201d symbol in the header row to open the \u201cAdd Column\u201d menu. The first option in the menu, \u201cAdd Column\u201d, will add a new, empty data column to your table. If you know what column type you need, the next option allows you to assign a type for your new column. Select a column type from the expanded menu. The third option, \u201cAdd formula column\u201d, will add a new formula column with the formula entry box immediately opened so you can begin entering your formula without additional clicks. \u201cHidden Columns\u201d expands to show a list of columns hidden from this view that can be quickly added back. \u201cLookups\u201d allows you to add data columns from related tables. You can use reference columns to relate data in different tables. Learn more about References and Lookups . \u201cShortcuts\u201d lists the most frequently used trigger formula functions. Learn more about each shortcut option at the links below: Timestamp Authorship Detect Duplicates in\u2026 UUID","title":"Adding and removing columns"},{"location":"col-types/#reordering-columns","text":"To reorder a column, first select the column if it isn\u2019t already selected, by clicking on the column header. Next, click and hold on the column header. After a second or two, you\u2019ll be able to drag the entire column to its new location. Another way to reorder columns is via the widget options: In the visible columns section, the columns can be dragged around freely to reorder them. You can also hide columns here.","title":"Reordering columns"},{"location":"col-types/#renaming-columns","text":"You can rename columns in several ways. One way is to double click a column header. Then, you can rename the column or add a column description. You can also hover on the column header, click on the drop-down, then select \u201cRename Column\u201d. This opens the same pop up seen above. Selecting \u201cColumn Options\u201d in the same drop-down opens the creator panel. From here, you can edit the Column Label, shown at the top or add a description. A bonus with this method is that you can also control the identifier given to the column in formulas. By default this is based on the field name, with any characters Python doesn\u2019t like replaced with \u201c_\u201c, and a number added if needed to keep the name unique within your table. If you don\u2019t like this identifier, you can change it, though it will still need to be Python-friendly. Click the link icon to make the ID field editable, then enter the new Column ID.","title":"Renaming columns"},{"location":"col-types/#formatting-columns","text":"Header and cell styles can be modified under the Column tab of the creator panel. When you open the styling menu, you have the option to apply text formatting as well as text and fill colors. If you want more color options, click the fill color box then find the shade you want. If you have a specific color you wish to use, you can enter the hex code, RGB or HSL values. Cell style can also be changed based on conditional rules. Learn more about Conditional Formatting .","title":"Formatting columns"},{"location":"col-types/#specifying-a-type","text":"Grist columns have types, similar to other spreadsheets or databases. The type of a column controls its appearance and the help Grist will offer you when editing cells. When you create a new column, it initially has the Any type. When you enter the column\u2019s first cell, Grist tries to narrow this type. If you enter a number, the column will be changed to Numeric type, which is right-aligned by default. If you enter something that doesn\u2019t look like a number, the column will be changed to Text type, which is left-aligned by default. To inspect the type of a column, hover over the column header, then click on the drop-down, then select \u201cColumn Options\u201d. The \u201cColumn Type\u201d section is what you are looking for. You will often want to control the column type manually. You can change it in the \u201cColumn Type\u201d section. For example, here we set a column full of \u201cyes\u201d and \u201cno\u201d responses to be of type Toggle : One advantage of doing so is that Grist can now offer you ways to visualize the column that are specialized to on/off style values. Each column type has different options in the \u201cCell Format\u201d section of the side panel: Regardless of the column type, you can enter any value in cells. If a value entered is incompatible with the defined type, the cell will be highlighted with an error (and columns referencing the invalid value will also display an error):","title":"Specifying a type"},{"location":"col-types/#supported-types","text":"Grist supports the following types: Type Description Text ( Default ) Any string of text. Numeric Floating point numbers. Integer Integers (whole numbers). Toggle Boolean (True / False) Date Valid date (without a time component). DateTime Valid date + time. Choice Single value from a list of pre-defined valid values. Choice List Multiple values from a list of pre-defined valid values. Reference A reference column to another table. Reference List A list of references to another table. Attachment Cells where you can place files or images.","title":"Supported types"},{"location":"col-types/#text-columns","text":"You can put any text you like in this type of column. For formatting, you can control alignment and word-wrap, text color and background color. If the column is used for storing web links, you can turn on \u201cHyperLink\u201d formatting to make links prettier and to include a clickable link icon.","title":"Text columns"},{"location":"col-types/#hyperlinks","text":"When a Text column uses \u201cHyperLink\u201d formatting, values get formatted like so: https://getgrist.com will show https://getgrist.com . Grist Labs https://getgrist.com will show Grist Labs (linking to \u201chttps://getgrist.com\u201d with \u201cGrist Labs\u201d as the text). Email Help mailto:support@getgrist.com will show Email Help , a link which would open an email program to compose an email to support@getgrist.com. In general, the value until the last space is used as the link text, while the last word is used as the link destination. Link formatting is particularly useful when links are generated using a formula such as: $Company + \" \" + $Website","title":"Hyperlinks"},{"location":"col-types/#numeric-columns","text":"This type is for numbers, including floating-point numbers. In addition to controlling alignment and color, you can choose the number format, and the minimum and maximum number of digits to show after the decimal point. Choosing the \u201cSpinner\u201d option for CELL FORMAT will show arrows in each cell for increasing/decreasing the number. The options under NUMBER FORMAT include: $ : Format for currency amounts, such as dollars or euro. Selecting the $ will add a currency prefix, thousands separators, and default to 2 digits after the decimal point. It will also open a currency selector for international currencies. Setting Default Currency You can set a document\u2019s default timezone, locale, and currency in Document Settings . , : Turn on the display of thousands separators. % : Show numbers as percentages. E.g. \u201c0.5\u201d would show as \u201c50%\u201d. Exp : Show numbers in exponential (or scientific) notation. E.g. \u201c1234\u201d would be shown as \u201c1.234E3\u201d. (-) : Show negative numbers in parentheses, without a leading minus sign. This is commonly used in accounting, and usually combined with $ or , formats.","title":"Numeric columns"},{"location":"col-types/#integer-columns","text":"This is strictly for whole numbers. It has the same options as the numeric type.","title":"Integer columns"},{"location":"col-types/#toggle-columns","text":"This type is for storing true/false values. The values can be shown as text, checkboxes, or switches. See also example in Specifying a type .","title":"Toggle columns"},{"location":"col-types/#date-columns","text":"This type is for storing calendar dates (without a time of day component). More details in Working with dates . You can choose the format for dates, see the date formatting reference .","title":"Date columns"},{"location":"col-types/#datetime-columns","text":"This type is for storing calendar dates plus time of day. More details in Working with dates . You can choose the format for dates, see the date and time formatting reference . You can also specify the timezone to display for. If you\u2019d like to set a default timezone for your document, you can do so in Document Settings .","title":"DateTime columns"},{"location":"col-types/#choice-columns","text":"This type is for storing one of a set of valid values, where you get to specify the available values. There\u2019s an example of using this type of column in the Lightweight CRM example . If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. You can add or remove choices by either clicking Edit or on the Choices box. To add a choice, type its value in the text field below the other choices and press Enter . To remove a choice, click the delete icon to the right of the choice or select the choice by clicking on it, and then press Backspace / Delete . To apply/save your changes, you can either click the Save button or press Enter . To discard your changes, you can either click the Cancel button or press Escape . Clicking the color dropdown to the left of a choice will open a color picker for customizing the fill and text color of a choice. Changes to colors are reflected inside cells and throughout the rest of your document once you save your changes. Choices can be re-arranged by clicking and dragging them, which determines the order in which they appear when typing into a cell. You can also rename a choice by clicking on it and typing a new name. Renaming a choice will also rename all the values that are used in your document. The configuration editor supports many convenient keyboard shortcuts. You can press the Up Arrow and Down Arrow keys to navigate between selected choices; hold the Shift key to bulk select adjacent choices while clicking or using the arrow keys; and hold the Command / Control key to multi-select choices while clicking. To select all choices, you can press Command / Control + A . Undo and redo are also supported in the configuration editor. While your cursor has focus on the editor text field, you can press Command / Control + Z to undo your last change, and Command / Control + Shift + Z to redo it. The configuration editor also supports copy and paste. To copy, select the choices you want copied and press Command / Control + C . To paste, focus on the text field and press Command / Control + V . Choices are pasted in bulk if the clipboard contains multiple lines of text. Choices can also be copied from one column\u2019s configuration editor and pasted into another, which will copy over both the values and their configured colors. When typing into a Choice column cell, your configured choices will be shown in an autocomplete menu. You can either click on a choice, or use the arrow keys and Enter to add a choice to a cell. If your input is not one of the valid choices, Grist will display a menu option for conveniently adding it as a valid choice and into the cell in one step.","title":"Choice columns"},{"location":"col-types/#choice-list-columns","text":"This type is for storing multiple values from a set of valid values, where you get to specify the available values. If you start off with a populated text column, Grist will take all unique values from that column as the valid choices. Choice List columns are configured the same way Choice columns are, and support the same level of customization and keyboard shortcuts. They differ in the number of choices they allow to be entered in each cell. While Choice columns only allow at most one value in a cell, Choice List columns allow many. Like with Choice columns, when typing into a Choice List column cell, the valid choices will be shown in an autocomplete menu. Once you\u2019ve selected a value, you can continue adding choices to the same cell. Choices can be re-arranged inside their cells by clicking and dragging them while the cell is being edited. You can also use the arrow keys and the Delete key to navigate and delete choices, or simply click the delete icon when hovering your cursor over a choice. Filtering Choice and Choice List columns\u2019 dropdown lists When entering data into a Choice or Choice List column you will see a dropdown list of all available choices. Sometimes it would be useful to filter the dropdown list based on a condition, such as the value in another cell. Writing conditions to filter choice dropdown lists is similar to filtering reference column\u2019s dropdown lists .","title":"Choice List columns"},{"location":"col-types/#reference-columns","text":"This sets up a cross-reference to another table. You can specify the table to reference, and a column within that table to show. There\u2019s a lot you can do with this kind of column, see Reference columns for details.","title":"Reference columns"},{"location":"col-types/#reference-list-columns","text":"Like Reference columns , but can store multiple references in a single cell. There\u2019s a lot you can do with this kind of column, see Reference columns for details.","title":"Reference List columns"},{"location":"col-types/#attachment-columns","text":"This column type lets you insert entire files and images in cells. When images are added in cells, a preview thumbnail is shown in the cell. The \u201cSize\u201d bar gives control of the scale of this thumbnail. When you create an attachment column, cells of that column will have a paperclip icon: When you click on a paperclip icon, you can select a file to attach. If it is an image, you\u2019ll see a thumbnail of it in the cell. If you hover over the image, you\u2019ll see a paperclip icon again, which you can use to add more files to the same cell. You\u2019ll also see an \u201copen-eye\u201d icon, which when clicked brings up a larger view of all of the cell\u2019s attachments, and gives you a way to rename them, download them, or remove them.","title":"Attachment columns"},{"location":"conditional-formatting/","text":"Conditional Formatting # Cell style can change based on conditional rules. Conditional rules are written as formulas. Conditional formatting can apply to an entire row or cells in a column. To add conditional formatting to a particular column, select the column, go to the CELL STYLE section of the creator panel under the Column tab, and click on Add conditional style . In this example, we have a list of dog breeders who have raised champion thoroughbreds. Let\u2019s apply conditional formatting to the Breeder column based on the number of champion dogs. We would like to highlight in gold any breeders with more than 2 champions. Here the conditional formula is $Number_of_Champions > 2 . We would also like to highlight breeders with 1 or 2 champion dogs in blue, and 0 champion dogs in brown. Click Add another rule to add more conditional styles. To add conditional formatting to rows, go to the ROW STYLE section of the creator panel under the Table > Widget tab, and click on Add conditional style . Order of Rules # Note that Grist applies the rules in order. Styles applied by later rules will override those applied by earlier rules. What would happen if we swapped the last two rules in the example above? Notice that Gen Hamamoto, who has 0 champion dogs, is not highlighted in brown. This is because after applying the second conditional style, $Number_of_Champions == 0 , Grist applied the third, $Number_of_Champions <= 2 , which applies to Gen Hamamoto as well and shades him blue.","title":"Conditional Formatting"},{"location":"conditional-formatting/#conditional-formatting","text":"Cell style can change based on conditional rules. Conditional rules are written as formulas. Conditional formatting can apply to an entire row or cells in a column. To add conditional formatting to a particular column, select the column, go to the CELL STYLE section of the creator panel under the Column tab, and click on Add conditional style . In this example, we have a list of dog breeders who have raised champion thoroughbreds. Let\u2019s apply conditional formatting to the Breeder column based on the number of champion dogs. We would like to highlight in gold any breeders with more than 2 champions. Here the conditional formula is $Number_of_Champions > 2 . We would also like to highlight breeders with 1 or 2 champion dogs in blue, and 0 champion dogs in brown. Click Add another rule to add more conditional styles. To add conditional formatting to rows, go to the ROW STYLE section of the creator panel under the Table > Widget tab, and click on Add conditional style .","title":"Conditional Formatting"},{"location":"conditional-formatting/#order-of-rules","text":"Note that Grist applies the rules in order. Styles applied by later rules will override those applied by earlier rules. What would happen if we swapped the last two rules in the example above? Notice that Gen Hamamoto, who has 0 champion dogs, is not highlighted in brown. This is because after applying the second conditional style, $Number_of_Champions == 0 , Grist applied the third, $Number_of_Champions <= 2 , which applies to Gen Hamamoto as well and shades him blue.","title":"Order of Rules"},{"location":"copying-docs/","text":"Copying Documents # It is sometimes useful to make a copy, or clone, of a Grist document. A few scenarios are described below. In all cases, you would start with the \u201cShare\u201d menu available from the top bar of an open document: Trying Out Changes # As your document grows in importance, it becomes riskier to make changes to its structure or logic. That\u2019s a good reason to work out such changes on a copy of the document, without fear of affecting the original. Open the Share menu and click the option \u201cWork on a Copy\u201d. You\u2019ll get an unsaved copy of your document. This copy is special in that it knows which original document it came from (you can see the original document ID included in its URL). You can experiment on this copy, making changes big or small, one or many. For those familiar with software development, this option is similar to a branch or a fork as used in version control systems like Git . When working on a copy, the Share menu has some new options. For example, you can view differences between the document copy and the original, using the \u201cCompare to Original\u201d menu item: New material will be highlighted in green, and old material in red. Once satisfied with your changes, click the option \u201cReplace Original\u201d. Your copy will replace the original document. Grist will warn you if the replacement risks overwriting any recent changes in the original. To discard your changes, simply go back to the original document using the \u201cReturn to Original\u201d option (or the Back button in your browser). Don\u2019t worry about cleaning up your copy. It does not count against any document limits, and will get cleaned up automatically if it has been unused for a while. You can also save your copy under a new name using the \u201cSave Copy\u201d option. Access to Unsaved Copies # When you create an experimental copy as described above, it gets a unique link. The copy isn\u2019t listed anywhere, so others will not find it unless you share this link. Anyone with a link to your copy and with access to the original document is allowed to view the copy, but you are the only user allowed to edit it. This means that you can share a link to your copy with others, who can review your changes. It also means that you can try out changes even if you cannot edit the original! You can then share a link to your copy with a collaborator who has edit access, who would be able to review your changes and apply them to the original document. Duplicating Documents # You can save your document under a new name using the \u201cDuplicate Document\u201d option in the Share menu. Clicking it opens a dialog: Type in the new name. If you have access to one or more team accounts, you may have a choice of a destination team and a destination workspace where to save the copy. Note that on a team site, you would not be able to save the document outside of the team site unless you have owner-level access to the document. Copying as a Template # If you mark the \u201cAs Template\u201d checkbox when saving a copy, you\u2019ll get a document which has all the structure, formulas, and layouts of the original, but none of the data. It makes it easy to use the existing structure for a new set of data. Copying for Backup Purposes # You can use the \u201cDuplicate Document\u201d option to save the current version of the document as a backup, perhaps appending today\u2019s date to the name of the copy. That said, Grist already makes automatic backups regularly, which may be sufficient for most backup needs. See Automatic Backups . Copying Public Examples # When you open a public example from the Examples & Templates page , it will open the example in fiddle mode . Fiddle mode is similar to working on a copy, as described above in Trying Out Changes . You may make changes, but they remain private to you. You can save a copy of the example under a new name using the \u201cSave Copy\u201d button or menu option. You can use the \u201cAs Template\u201d checkbox to discard the data of the example, keeping only its structure. This makes it easy to start using it for your own data.","title":"Copying documents"},{"location":"copying-docs/#copying-documents","text":"It is sometimes useful to make a copy, or clone, of a Grist document. A few scenarios are described below. In all cases, you would start with the \u201cShare\u201d menu available from the top bar of an open document:","title":"Copying Documents"},{"location":"copying-docs/#trying-out-changes","text":"As your document grows in importance, it becomes riskier to make changes to its structure or logic. That\u2019s a good reason to work out such changes on a copy of the document, without fear of affecting the original. Open the Share menu and click the option \u201cWork on a Copy\u201d. You\u2019ll get an unsaved copy of your document. This copy is special in that it knows which original document it came from (you can see the original document ID included in its URL). You can experiment on this copy, making changes big or small, one or many. For those familiar with software development, this option is similar to a branch or a fork as used in version control systems like Git . When working on a copy, the Share menu has some new options. For example, you can view differences between the document copy and the original, using the \u201cCompare to Original\u201d menu item: New material will be highlighted in green, and old material in red. Once satisfied with your changes, click the option \u201cReplace Original\u201d. Your copy will replace the original document. Grist will warn you if the replacement risks overwriting any recent changes in the original. To discard your changes, simply go back to the original document using the \u201cReturn to Original\u201d option (or the Back button in your browser). Don\u2019t worry about cleaning up your copy. It does not count against any document limits, and will get cleaned up automatically if it has been unused for a while. You can also save your copy under a new name using the \u201cSave Copy\u201d option.","title":"Trying Out Changes"},{"location":"copying-docs/#access-to-unsaved-copies","text":"When you create an experimental copy as described above, it gets a unique link. The copy isn\u2019t listed anywhere, so others will not find it unless you share this link. Anyone with a link to your copy and with access to the original document is allowed to view the copy, but you are the only user allowed to edit it. This means that you can share a link to your copy with others, who can review your changes. It also means that you can try out changes even if you cannot edit the original! You can then share a link to your copy with a collaborator who has edit access, who would be able to review your changes and apply them to the original document.","title":"Access to Unsaved Copies"},{"location":"copying-docs/#duplicating-documents","text":"You can save your document under a new name using the \u201cDuplicate Document\u201d option in the Share menu. Clicking it opens a dialog: Type in the new name. If you have access to one or more team accounts, you may have a choice of a destination team and a destination workspace where to save the copy. Note that on a team site, you would not be able to save the document outside of the team site unless you have owner-level access to the document.","title":"Duplicating Documents"},{"location":"copying-docs/#copying-as-a-template","text":"If you mark the \u201cAs Template\u201d checkbox when saving a copy, you\u2019ll get a document which has all the structure, formulas, and layouts of the original, but none of the data. It makes it easy to use the existing structure for a new set of data.","title":"Copying as a Template"},{"location":"copying-docs/#copying-for-backup-purposes","text":"You can use the \u201cDuplicate Document\u201d option to save the current version of the document as a backup, perhaps appending today\u2019s date to the name of the copy. That said, Grist already makes automatic backups regularly, which may be sufficient for most backup needs. See Automatic Backups .","title":"Copying for Backup Purposes"},{"location":"copying-docs/#copying-public-examples","text":"When you open a public example from the Examples & Templates page , it will open the example in fiddle mode . Fiddle mode is similar to working on a copy, as described above in Trying Out Changes . You may make changes, but they remain private to you. You can save a copy of the example under a new name using the \u201cSave Copy\u201d button or menu option. You can use the \u201cAs Template\u201d checkbox to discard the data of the example, keeping only its structure. This makes it easy to start using it for your own data.","title":"Copying Public Examples"},{"location":"creating-doc/","text":"Creating a document # To get started with Grist you\u2019ll first need to create a document. Each document can store different kinds of data, so think of it as a collection of related data. You can start from scratch or import existing data. Click the \u201cAdd New\u201d button on the home screen and choose either \u201cCreate empty document\u201d or \u201cImport document\u201d. You can import spreadsheets in Excel format, as well as comma-separated value files or CSVs. If you import a spreadsheet that has multiple tabs, each tab will become a separate table in Grist. Examples and templates # The \u201cExamples & Templates\u201d list on the Grist homepage includes a number of Grist documents that demonstrate Grist features and use cases. These are always accessible via a link on the bottom left of the home page. For each example, there is a corresponding How-To Tutorial in the Grist Help Center, which describes how to use it or how to build such a document from scratch. When you open an example, you can make changes to it. You are editing a copy, and your changes are private to you. You can save your changes by clicking the \u201cSave Copy\u201d button on the top of the page. You can also use any of the examples as a template for a new document, whch will include the structure, layout, and formulas of the example, but none of the sample data. Simply use the \u201cSave Copy\u201d button and check the \u201cAs Template\u201d checkbox in the dialog that opens. Importing more data # Once you\u2019ve created a document, you can import more data into it by opening the document and selecting \u201cAdd New\u201d and then one of the Import options. You can read more about importing data at Importing more data . Document settings # While a document is open, you can access the document\u2019s settings from the user menu or from the Tools menu in the left-hand navigation panel. There you can find the document\u2019s ID for API use, as well as set the default timezone, locale, and currency for the document.","title":"Creating a document"},{"location":"creating-doc/#creating-a-document","text":"To get started with Grist you\u2019ll first need to create a document. Each document can store different kinds of data, so think of it as a collection of related data. You can start from scratch or import existing data. Click the \u201cAdd New\u201d button on the home screen and choose either \u201cCreate empty document\u201d or \u201cImport document\u201d. You can import spreadsheets in Excel format, as well as comma-separated value files or CSVs. If you import a spreadsheet that has multiple tabs, each tab will become a separate table in Grist.","title":"Creating a document"},{"location":"creating-doc/#examples-and-templates","text":"The \u201cExamples & Templates\u201d list on the Grist homepage includes a number of Grist documents that demonstrate Grist features and use cases. These are always accessible via a link on the bottom left of the home page. For each example, there is a corresponding How-To Tutorial in the Grist Help Center, which describes how to use it or how to build such a document from scratch. When you open an example, you can make changes to it. You are editing a copy, and your changes are private to you. You can save your changes by clicking the \u201cSave Copy\u201d button on the top of the page. You can also use any of the examples as a template for a new document, whch will include the structure, layout, and formulas of the example, but none of the sample data. Simply use the \u201cSave Copy\u201d button and check the \u201cAs Template\u201d checkbox in the dialog that opens.","title":"Examples and templates"},{"location":"creating-doc/#importing-more-data","text":"Once you\u2019ve created a document, you can import more data into it by opening the document and selecting \u201cAdd New\u201d and then one of the Import options. You can read more about importing data at Importing more data .","title":"Importing more data"},{"location":"creating-doc/#document-settings","text":"While a document is open, you can access the document\u2019s settings from the user menu or from the Tools menu in the left-hand navigation panel. There you can find the document\u2019s ID for API use, as well as set the default timezone, locale, and currency for the document.","title":"Document settings"},{"location":"custom-layouts/","text":"Custom Layouts # You can easily add multiple widgets to one page, as described in Page widgets , and link them as described in Linking widgets . It is also easy to customize their arrangement. Move the mouse cursor over the title of the widget. A small drag icon will appear over to the left of the title. When you press this icon, you can drag the entire widget to a different place on the screen. As you move it close to the edge of the screen, or the edge of another widget, you\u2019ll see a dashed outline \u2013 sometimes more than one \u2013 where the widget can be dropped. Release the mouse to reposition this widget. To resize widgets, move the mouse cursor between two widgets until a dashed line appear. Drag this dashed line to resize. To expand a widget, click the expand icon at the upper-right of the widget. This will open a full-page view of the widget. To collapse a widget, click and drag a widget to the widget tray at the top of the page. When you click on a collapsed widget, it opens in a full-page view. To restore it to the main page, just drag the collapsed widget to the desired location. Layout recommendations # While there is no limit to how complicated a layout you can create, you should aim for simple layouts that will be easy to use for your users (even when you are the only user!) One rule of thumb is that a widget controlled by another \u201cselector\u201d widget (see Linking widgets ) should be to the right or below it. Here are some common layouts. Layout: List and detail # The most common one is to have a list of items on the left, with one or more widgets on the right providing more information. For instance, the Lightweight CRM example includes a list of people on the left, with a person\u2019s card and a table of related interactions to the right of the list. In this usage, you might want to include in the list only the minimal information you need, perhaps only a contact\u2019s name. If your table has many columns, a quick way to leave only a few is via the widget options in the right-side panel. In the table widget, click the three-dot menu on the top right, and select \u201cWidget options\u201d. You\u2019ll see a list of \u201cVisible Columns\u201d. Click \u201cSelect all\u201d link on top of that list: Now uncheck the few fields you want to keep, and click \u201cHide columns\u201d to hide the rest. Layout: Spreadsheet plus # Sometimes a wide spreadsheet with many columns is convenient. If you\u2019d like to see more info associated with the rows of this spreadsheet, you can add widgets below it. These could be details linked to the spreadsheet, or summary tables that show totals or other global info. For instance, here is a possible layout based on the Lightweight CRM example. It shows contacts as a wide spreadsheet, and below that includes sections with an overall summary, and interactions for the selected contact. Layout: Summary and details # Sometimes it\u2019s useful to divide up a large dataset into subsets. For instance, you might have credit card transactions, and want a way to view them one month at a time. To do it, you\u2019ll use a \u201cMonth\u201d column, creating one with a formula if needed. Then create a summary table grouped by \u201cMonth\u201d (see Summary tables , and link the table of transactions to it. We can then select a month and see a spreadsheet of only the transactions in that month. Layout: Charts dashboard # If you have many charts, you can just lay them out in a grid to create a top-level dashboard. For dynamic charts in which data is selected by another table, a layout like List-and-detail above would work well. One tip is to include both a Table widget and a Chart widget, configured and linked the same way, and differing only in the widget type: Having a table alongside the chart can be a useful reference, as well as provide more context to what is visible in the chart.","title":"Custom layouts"},{"location":"custom-layouts/#custom-layouts","text":"You can easily add multiple widgets to one page, as described in Page widgets , and link them as described in Linking widgets . It is also easy to customize their arrangement. Move the mouse cursor over the title of the widget. A small drag icon will appear over to the left of the title. When you press this icon, you can drag the entire widget to a different place on the screen. As you move it close to the edge of the screen, or the edge of another widget, you\u2019ll see a dashed outline \u2013 sometimes more than one \u2013 where the widget can be dropped. Release the mouse to reposition this widget. To resize widgets, move the mouse cursor between two widgets until a dashed line appear. Drag this dashed line to resize. To expand a widget, click the expand icon at the upper-right of the widget. This will open a full-page view of the widget. To collapse a widget, click and drag a widget to the widget tray at the top of the page. When you click on a collapsed widget, it opens in a full-page view. To restore it to the main page, just drag the collapsed widget to the desired location.","title":"Custom Layouts"},{"location":"custom-layouts/#layout-recommendations","text":"While there is no limit to how complicated a layout you can create, you should aim for simple layouts that will be easy to use for your users (even when you are the only user!) One rule of thumb is that a widget controlled by another \u201cselector\u201d widget (see Linking widgets ) should be to the right or below it. Here are some common layouts.","title":"Layout recommendations"},{"location":"custom-layouts/#layout-list-and-detail","text":"The most common one is to have a list of items on the left, with one or more widgets on the right providing more information. For instance, the Lightweight CRM example includes a list of people on the left, with a person\u2019s card and a table of related interactions to the right of the list. In this usage, you might want to include in the list only the minimal information you need, perhaps only a contact\u2019s name. If your table has many columns, a quick way to leave only a few is via the widget options in the right-side panel. In the table widget, click the three-dot menu on the top right, and select \u201cWidget options\u201d. You\u2019ll see a list of \u201cVisible Columns\u201d. Click \u201cSelect all\u201d link on top of that list: Now uncheck the few fields you want to keep, and click \u201cHide columns\u201d to hide the rest.","title":"Layout: List and detail"},{"location":"custom-layouts/#layout-spreadsheet-plus","text":"Sometimes a wide spreadsheet with many columns is convenient. If you\u2019d like to see more info associated with the rows of this spreadsheet, you can add widgets below it. These could be details linked to the spreadsheet, or summary tables that show totals or other global info. For instance, here is a possible layout based on the Lightweight CRM example. It shows contacts as a wide spreadsheet, and below that includes sections with an overall summary, and interactions for the selected contact.","title":"Layout: Spreadsheet plus"},{"location":"custom-layouts/#layout-summary-and-details","text":"Sometimes it\u2019s useful to divide up a large dataset into subsets. For instance, you might have credit card transactions, and want a way to view them one month at a time. To do it, you\u2019ll use a \u201cMonth\u201d column, creating one with a formula if needed. Then create a summary table grouped by \u201cMonth\u201d (see Summary tables , and link the table of transactions to it. We can then select a month and see a spreadsheet of only the transactions in that month.","title":"Layout: Summary and details"},{"location":"custom-layouts/#layout-charts-dashboard","text":"If you have many charts, you can just lay them out in a grid to create a top-level dashboard. For dynamic charts in which data is selected by another table, a layout like List-and-detail above would work well. One tip is to include both a Table widget and a Chart widget, configured and linked the same way, and differing only in the widget type: Having a table alongside the chart can be a useful reference, as well as provide more context to what is visible in the chart.","title":"Layout: Charts dashboard"},{"location":"data-security/","text":"Data Security # Grist is available as a hosted service (\u201cGrist SaaS\u201d) running on infrastructure managed by Grist Labs. It can also be installed on your own infrastructure (\u201cSelf-Managed Grist\u201d). In either case, we take measures to secure your data that you should know about. Grist SaaS # Our general privacy policy and terms are at https://www.getgrist.com/privacy and https://www.getgrist.com/terms . In addition, here is a summary of specific measures that relate to Grist documents that we host on your behalf: Grist servers operate in the Amazon Web Services (AWS) cloud infrastructure, in the United States. AWS S3 is used for long-term storage, and stores documents in encrypted form. Data is stored in the United States. Grist employees never look at your data and cannot open your documents. The one exception is if you choose to share a document with customer support in order to get help with an issue. When being operated on, your data will by necessity exist in unencrypted form in some of Grist\u2019s internal systems. Only select key employees have full access to these systems, and policy prohibits them from looking inside documents. Secure HTTPS is used for all access to Grist via public internet (both website and API calls). Regular backups of Grist documents are made, and are stored in encrypted form. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. If you delete a document, it will persist for 30 days in a Trash folder under your control. At any time a \u201cDelete forever\u201d option can be used to immediately purge a document in this folder. All automatic backups are purged along with the document. The hosted Grist product has not at this time gone through certification or auditing for SOC 2, ISO 27001, HIPAA, or GDPR compliance. If you need specific documentation, please contact customer support. You can also nudge us to prioritize certification over feature development at this issue . Self-Managed Grist # For Self-Managed Grist, you are in complete control of where servers operate and where data is stored. Here are some security considerations to bear in mind: Grist software is distributed via the gristlabs organization on github and docker hub . Please exercise diligence if accessing software elsewhere, since the software you install will have full access to your data. Grist documents support powerful Python formulas. Please pay attention to instructions for configuring sandboxing if your team may be working with untrusted documents. Grist by default is welcoming to anonymous users, allowing them to create and edit their own documents. You may wish to configure a stricter arrangement . Grist does not make external services mandatory, since that would introduce unnecessary obstacles in some scenarios. For example, an individual editing a Grist document offline on their own desktop shouldn\u2019t need to install a PostgreSQL database first. But it is important to evaluate what you need in your situation rather than simply sticking with the defaults. Please read about the data Grist stores and your options for where to store it. It is important to keep your Grist installation up to date .","title":"Data Security"},{"location":"data-security/#data-security","text":"Grist is available as a hosted service (\u201cGrist SaaS\u201d) running on infrastructure managed by Grist Labs. It can also be installed on your own infrastructure (\u201cSelf-Managed Grist\u201d). In either case, we take measures to secure your data that you should know about.","title":"Data Security"},{"location":"data-security/#grist-saas","text":"Our general privacy policy and terms are at https://www.getgrist.com/privacy and https://www.getgrist.com/terms . In addition, here is a summary of specific measures that relate to Grist documents that we host on your behalf: Grist servers operate in the Amazon Web Services (AWS) cloud infrastructure, in the United States. AWS S3 is used for long-term storage, and stores documents in encrypted form. Data is stored in the United States. Grist employees never look at your data and cannot open your documents. The one exception is if you choose to share a document with customer support in order to get help with an issue. When being operated on, your data will by necessity exist in unencrypted form in some of Grist\u2019s internal systems. Only select key employees have full access to these systems, and policy prohibits them from looking inside documents. Secure HTTPS is used for all access to Grist via public internet (both website and API calls). Regular backups of Grist documents are made, and are stored in encrypted form. Grist retains more frequent snapshots of recent changes, and less frequent ones as you go back in time. If you delete a document, it will persist for 30 days in a Trash folder under your control. At any time a \u201cDelete forever\u201d option can be used to immediately purge a document in this folder. All automatic backups are purged along with the document. The hosted Grist product has not at this time gone through certification or auditing for SOC 2, ISO 27001, HIPAA, or GDPR compliance. If you need specific documentation, please contact customer support. You can also nudge us to prioritize certification over feature development at this issue .","title":"Grist SaaS"},{"location":"data-security/#self-managed-grist","text":"For Self-Managed Grist, you are in complete control of where servers operate and where data is stored. Here are some security considerations to bear in mind: Grist software is distributed via the gristlabs organization on github and docker hub . Please exercise diligence if accessing software elsewhere, since the software you install will have full access to your data. Grist documents support powerful Python formulas. Please pay attention to instructions for configuring sandboxing if your team may be working with untrusted documents. Grist by default is welcoming to anonymous users, allowing them to create and edit their own documents. You may wish to configure a stricter arrangement . Grist does not make external services mandatory, since that would introduce unnecessary obstacles in some scenarios. For example, an individual editing a Grist document offline on their own desktop shouldn\u2019t need to install a PostgreSQL database first. But it is important to evaluate what you need in your situation rather than simply sticking with the defaults. Please read about the data Grist stores and your options for where to store it. It is important to keep your Grist installation up to date .","title":"Self-Managed Grist"},{"location":"dates/","text":"Overview # Grist expresses dates and times in two ways. The first is the Date column type, which represents a calendar date, with no time of day, and not associated with any particular timezone. The second is the DateTime column type, which represents a calendar date with a time of day which can be linked with a timezone. The Date and DateTime column types support different formatting options. When a column is set to be a Date or a DateTime , a date-picker widget will let you select the date on a calendar when editing a cell. When working with dates in formulas, the dates are Python datetime objects . That allows you to do some powerful things, but can be unexpected if you\u2019re not familiar with them. Making a date/time column # For a general introduction to setting the type of columns, see Columns and data types . To tell Grist that you intend to enter only date/times in a column, over on the header for the column, find the drop-down, and select \u201cColumn Options\u201d. Then in the side panel that opens on the right, pick \u201cDate\u201d from the \u201cColumn Type\u201d drop-down. Or, if you want dates with times, pick \u201cDateTime\u201d. Then you can choose your preferred date/time format. For the \u201cDateTime\u201d type, you can also choose the timezone. When you convert a column from another type, such as \u201cText\u201d, you\u2019ll see a preview of the conversion results, and will need to click \u201cApply\u201d to complete conversion. You can come back and change settings at any time. Now when you edit a cell in this column, you will have help for selecting dates and times. Inserting the current date # You can insert the current date in a cell using \u2318 + ; (semicolon) (Mac) or Ctrl + ; (Windows). You can insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows). When editing a date cell, the date entry widget has a \u201ctoday\u201d button for today\u2019s date. Parsing dates from strings # The DATEVALUE function converts a string that represents a date into a datetime object. It\u2019s simple to use and it will auto-detect different date formats: You can also use Python\u2019s datetime library, which provides two helpful functions: strptime() and strftime() . For example, let\u2019s say you have a table of movie sequels and their release dates (as strings). You\u2019d like to parse out the actual date to be able to sort the table properly. Here\u2019s how you would do that: First line imports the datetime library The second line splits the string into two parts and returns the second part (Python arrays are zero-based). The third line uses Python\u2019s strptime function to parse the date (e.g. \u201cMay 19, 1999\u201d) into a datetime object. The first parameter to the function is the string to parse, the second parameter is the date format that the string is in. Take a look at the format options to see if the example format string %B %d, %Y makes sense. (Note: You could\u2019ve also used DATEVALUE(d) to achieve the same result.) The result has a true date column and can now be properly sorted chronologically, with \u201cA New Hope\u201d at the top. For historical reasons, the first Star Wars movie is considered to be Episode 4. And, because the column type is selected as a date, you can use the \u201cDate Format\u201d in \u201cColumn Options\u201d to select the format in which to display the date. For some situations, you may wish to use the dateutil python library. For example, if you live in an area where dates typically start with the day and then the month, you could use this formula: import dateutil dateutil.parser.parse($date_text, dayfirst=True) Date arithmetic # Once you have a proper date column, often you\u2019ll want to do date arithmetic such as calculating the difference between two dates. The simplest way to do this is to use the DATEDIF function which takes two dates and the unit of information to return (Days, Months, or Years). You could also use the minus sign to subtract two dates, but you might be surprised at the result: This happens because subtracting two datetime objects as we did in the example above, results in a datetime.timedelta object which represents, \u201cA duration expressing the difference between two date, time, or datetime instances to microsecond resolution.\u201d In Grist (and Python) you have to be more specific above how you want to display the date difference. For example, to get the number of days from the returned timedelta object, use its .days property: If you want weeks or years, just divide by 7 or by 365. (Divide by 7.0 or 365.0 to include a fractional part in the result.) If you want hours, multiply by 24. You can also use specific functions to get what you want. For example, DAYS is a common function in spreadsheet apps that returns the difference between two dates: DAYS($Last_day, $First_day) Excel/Sheets formulas Grist supports many other common functions from other spreadsheet apps, including DATEADD , DATEDIF , DATEVALUE , MONTH , HOUR , and many more . Getting a part of the date # You\u2019ve seen how to parse the date, display it in different formats, and do date arithmetic. But what if you want to get more information about a specific date, such as getting its day of the week? One option is to use the WEEKDAY function, which behaves as it does in Excel, returning 1-7 for Sunday-Saturday. Alternatively, we can use the strftime function: Yet another option would be to reformat the date using Date Format in Column Options (see the date formatting reference ). Time zones # Values in DateTime columns represent moments in time. The same moment will look different in different timezones. In Grist, the timezone is set on each DateTime column. For instance, if the timezone is set to \u201cAmerica/New_York\u201d, it will show the values in New York timezone to collaborators anywhere in the world. A Grist document has a global timezone setting, which serves as the default timezone for when you create a new column of type DateTime . This global timezone is set to your local timezone when you first create a document. You can see or change it by clicking on your profile picture or icon, and selecting \u201cDocument Settings\u201d. If you insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows) into a DateTime column, it will be inserted as a true timezone-aware timestamp, and shown with the timezone set for that column. If you do the same in a Text column, the date/time will be inserted as the text appropriate for the document\u2019s global timezone setting. Similarly, inserting the current date into a Date column will produce the current date according to the document\u2019s timezone. Additional resources # Python cheatsheet for strftime , for using with strftime() and strptime() in formulas. Date formatting cheatsheet , for specifying the date/time format in column settings. dateutil library , extensions to the Python standard datetime module.","title":"Working with dates"},{"location":"dates/#overview","text":"Grist expresses dates and times in two ways. The first is the Date column type, which represents a calendar date, with no time of day, and not associated with any particular timezone. The second is the DateTime column type, which represents a calendar date with a time of day which can be linked with a timezone. The Date and DateTime column types support different formatting options. When a column is set to be a Date or a DateTime , a date-picker widget will let you select the date on a calendar when editing a cell. When working with dates in formulas, the dates are Python datetime objects . That allows you to do some powerful things, but can be unexpected if you\u2019re not familiar with them.","title":"Overview"},{"location":"dates/#making-a-datetime-column","text":"For a general introduction to setting the type of columns, see Columns and data types . To tell Grist that you intend to enter only date/times in a column, over on the header for the column, find the drop-down, and select \u201cColumn Options\u201d. Then in the side panel that opens on the right, pick \u201cDate\u201d from the \u201cColumn Type\u201d drop-down. Or, if you want dates with times, pick \u201cDateTime\u201d. Then you can choose your preferred date/time format. For the \u201cDateTime\u201d type, you can also choose the timezone. When you convert a column from another type, such as \u201cText\u201d, you\u2019ll see a preview of the conversion results, and will need to click \u201cApply\u201d to complete conversion. You can come back and change settings at any time. Now when you edit a cell in this column, you will have help for selecting dates and times.","title":"Making a date/time column"},{"location":"dates/#inserting-the-current-date","text":"You can insert the current date in a cell using \u2318 + ; (semicolon) (Mac) or Ctrl + ; (Windows). You can insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows). When editing a date cell, the date entry widget has a \u201ctoday\u201d button for today\u2019s date.","title":"Inserting the current date"},{"location":"dates/#parsing-dates-from-strings","text":"The DATEVALUE function converts a string that represents a date into a datetime object. It\u2019s simple to use and it will auto-detect different date formats: You can also use Python\u2019s datetime library, which provides two helpful functions: strptime() and strftime() . For example, let\u2019s say you have a table of movie sequels and their release dates (as strings). You\u2019d like to parse out the actual date to be able to sort the table properly. Here\u2019s how you would do that: First line imports the datetime library The second line splits the string into two parts and returns the second part (Python arrays are zero-based). The third line uses Python\u2019s strptime function to parse the date (e.g. \u201cMay 19, 1999\u201d) into a datetime object. The first parameter to the function is the string to parse, the second parameter is the date format that the string is in. Take a look at the format options to see if the example format string %B %d, %Y makes sense. (Note: You could\u2019ve also used DATEVALUE(d) to achieve the same result.) The result has a true date column and can now be properly sorted chronologically, with \u201cA New Hope\u201d at the top. For historical reasons, the first Star Wars movie is considered to be Episode 4. And, because the column type is selected as a date, you can use the \u201cDate Format\u201d in \u201cColumn Options\u201d to select the format in which to display the date. For some situations, you may wish to use the dateutil python library. For example, if you live in an area where dates typically start with the day and then the month, you could use this formula: import dateutil dateutil.parser.parse($date_text, dayfirst=True)","title":"Parsing dates from strings"},{"location":"dates/#date-arithmetic","text":"Once you have a proper date column, often you\u2019ll want to do date arithmetic such as calculating the difference between two dates. The simplest way to do this is to use the DATEDIF function which takes two dates and the unit of information to return (Days, Months, or Years). You could also use the minus sign to subtract two dates, but you might be surprised at the result: This happens because subtracting two datetime objects as we did in the example above, results in a datetime.timedelta object which represents, \u201cA duration expressing the difference between two date, time, or datetime instances to microsecond resolution.\u201d In Grist (and Python) you have to be more specific above how you want to display the date difference. For example, to get the number of days from the returned timedelta object, use its .days property: If you want weeks or years, just divide by 7 or by 365. (Divide by 7.0 or 365.0 to include a fractional part in the result.) If you want hours, multiply by 24. You can also use specific functions to get what you want. For example, DAYS is a common function in spreadsheet apps that returns the difference between two dates: DAYS($Last_day, $First_day) Excel/Sheets formulas Grist supports many other common functions from other spreadsheet apps, including DATEADD , DATEDIF , DATEVALUE , MONTH , HOUR , and many more .","title":"Date arithmetic"},{"location":"dates/#getting-a-part-of-the-date","text":"You\u2019ve seen how to parse the date, display it in different formats, and do date arithmetic. But what if you want to get more information about a specific date, such as getting its day of the week? One option is to use the WEEKDAY function, which behaves as it does in Excel, returning 1-7 for Sunday-Saturday. Alternatively, we can use the strftime function: Yet another option would be to reformat the date using Date Format in Column Options (see the date formatting reference ).","title":"Getting a part of the date"},{"location":"dates/#time-zones","text":"Values in DateTime columns represent moments in time. The same moment will look different in different timezones. In Grist, the timezone is set on each DateTime column. For instance, if the timezone is set to \u201cAmerica/New_York\u201d, it will show the values in New York timezone to collaborators anywhere in the world. A Grist document has a global timezone setting, which serves as the default timezone for when you create a new column of type DateTime . This global timezone is set to your local timezone when you first create a document. You can see or change it by clicking on your profile picture or icon, and selecting \u201cDocument Settings\u201d. If you insert the current date and time using \u2318 + Shift + ; (Mac) or Ctrl + Shift + ; (Windows) into a DateTime column, it will be inserted as a true timezone-aware timestamp, and shown with the timezone set for that column. If you do the same in a Text column, the date/time will be inserted as the text appropriate for the document\u2019s global timezone setting. Similarly, inserting the current date into a Date column will produce the current date according to the document\u2019s timezone.","title":"Time zones"},{"location":"dates/#additional-resources","text":"Python cheatsheet for strftime , for using with strftime() and strptime() in formulas. Date formatting cheatsheet , for specifying the date/time format in column settings. dateutil library , extensions to the Python standard datetime module.","title":"Additional resources"},{"location":"document-history/","text":"Document history # To access information about a document\u2019s history, click \u201cDocument History\u201d in the left panel. The right panel will then offer two tabs, \u201cActivity\u201d and \u201cSnapshots\u201d. Snapshots # Grist automatically saves complete backups of your documents as you work on them, making them available on the Snapshots tab. Read Automatic Backups for details on using this tab to examine, compare, and restore from older versions of a document. Activity # The Activity tab lists all recent changes for the table associated with the widget you\u2019re currently looking at. Select the \u201cAll tables\u201d checkbox to see recent changes in the document regardless of table. Changes are shown as small table extracts. The cells are clickable, and bring you to the current version of the cell (if it still exists) in the \u201cprimary\u201d widget for the table containing it (this is typically the first widget created showing the table).","title":"Document history"},{"location":"document-history/#document-history","text":"To access information about a document\u2019s history, click \u201cDocument History\u201d in the left panel. The right panel will then offer two tabs, \u201cActivity\u201d and \u201cSnapshots\u201d.","title":"Document history"},{"location":"document-history/#snapshots","text":"Grist automatically saves complete backups of your documents as you work on them, making them available on the Snapshots tab. Read Automatic Backups for details on using this tab to examine, compare, and restore from older versions of a document.","title":"Snapshots"},{"location":"document-history/#activity","text":"The Activity tab lists all recent changes for the table associated with the widget you\u2019re currently looking at. Select the \u201cAll tables\u201d checkbox to see recent changes in the document regardless of table. Changes are shown as small table extracts. The cells are clickable, and bring you to the current version of the cell (if it still exists) in the \u201cprimary\u201d widget for the table containing it (this is typically the first widget created showing the table).","title":"Activity"},{"location":"embedding/","text":"Embedding Grist # Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view: Parameters # Read-Only vs. Editable # Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing . Appearance # Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Embedding"},{"location":"embedding/#embedding-grist","text":"Perhaps you\u2019d like to list your product, prices and quantities on your website, or you want to display a pie chart of voting results that updates live. With public access turned on, you may embed your Grist document on your own site. To do that, you first need to make it public and have access to your website\u2019s code in order to place some HTML code in the desired location. If your site is hosted on some popular cloud CMS platform (like Blogger or WordPress), you may find HTML code blocks in your theme editor, or may need to use a plugin to access your site\u2019s code. If you have any problems editing your site, feel free to ask us or post a question on our Community Forum . Once you have decided where to embed your document, paste this code snippet in your HTML file: The src attribute points to the URL of the page you want to embed. To get the URL for your document, simply navigate to the page you want to share and copy the URL from the browser\u2019s address bar. Appending the ?embed=true parameter at the end tells Grist that it should show only the content of your page, removing any elements that are not necessary for the embedded version. You may wish to adjust height and width attributes to make it look better on your site. Since this Help Center document is a regular HTML file, we can try it right away! Below you should be able to see an embedded live table (not a screenshot) from one of our examples : This is a live, read-only view of the Grist page and it gets updated as soon as someone edits it. You can, of course, embed any page you wish, including card view, charts and any page with multiple sections. Here are two more examples with a chart and a card list view:","title":"Embedding Grist"},{"location":"embedding/#parameters","text":"","title":"Parameters"},{"location":"embedding/#read-only-vs-editable","text":"Appending a URL with ?embed=true shares the page as read-only while ?style=singlePage can be edited and follows access rules . Sharing an Embedded Style View Adding ?embed=true or ?style=singlePage to the end of a document\u2019s page URL renders the document without the toolbar on top, the page menu on the left or creator panel on the right. To learn more about link sharing, see Public access and link sharing .","title":"Read-Only vs. Editable"},{"location":"embedding/#appearance","text":"Control how an embedded iframe appears by assigning a theme appearance by appending ?themeAppearance=light for light mode or ?themeAppearance=dark for dark mode to your URL. You can also override the operating system\u2019s default using ?themeSyncWithOs=false . Chaining Parameters When adding parameters to the end of your URL, the first always leads with ? . Any additional parameters lead with & . For example, https://templates.getgrist.com/6D8E2h2DQNwS/Task-Management/p/6?embed=true&themeAppearance=dark&themeSyncWithOs=false creates an embedded-style, read-only view and forces dark mode, regardless of your OS settings.","title":"Appearance"},{"location":"enter-data/","text":"Entering data # A spreadsheet-like grid is a great way to see data. In Grist, this is the view offered by the default page widget called \u201cTable\u201d. As in a spreadsheet, you can use the mouse or arrow keys to move around the cells of a table. To start entering data into a selecte cell, either start typing, hit Enter , or double-click the cell. Editing cells # While editing a cell, several keys are special: Escape cancels the operation and restores the previous value in the cell. Tab , Shift + Tab saves your entry and moves your cursor to the next or previous cell. Enter saves your entry and moves your cursor to the next row. Shift + Enter adds a newline inside your cell. Copying and pasting # You can copy data from Grist or paste data into it. If the pasted range is longer than the available records, new records will be added. Note that Grist does not create new columns automatically. If the pasted data has more columns than the grid displays, extra columns will be omitted. Data entry widgets # In Grist, columns have types. In addition to typing in values, many column types offer specialized widgets for entering data more conveniently. Here are some of the most useful ones: Toggle . A \u201cToggle\u201d column shows True/False values, and can show them as a \u201cCheckbox\u201d or as a \u201cSwitch\u201d widget, which you can select in the column options. You can toggle a value in such a cell by clicking the check mark or the switch, or by hitting Space . Date and DateTime . Hitting Enter on such a cell will open a calendar to pick a date. Choice and Reference . Typing into a cell of one of these types will produce an auto-complete dropdown menu. Linking to cells # You can make a sharable link to a cell by pressing \u2318 \u21e7 A (Mac) or Ctrl + Shift + A (Windows) while the cell is selected. This option is also available via the row menu as \u201cCopy anchor link.\u201d The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document.","title":"Entering data"},{"location":"enter-data/#entering-data","text":"A spreadsheet-like grid is a great way to see data. In Grist, this is the view offered by the default page widget called \u201cTable\u201d. As in a spreadsheet, you can use the mouse or arrow keys to move around the cells of a table. To start entering data into a selecte cell, either start typing, hit Enter , or double-click the cell.","title":"Entering data"},{"location":"enter-data/#editing-cells","text":"While editing a cell, several keys are special: Escape cancels the operation and restores the previous value in the cell. Tab , Shift + Tab saves your entry and moves your cursor to the next or previous cell. Enter saves your entry and moves your cursor to the next row. Shift + Enter adds a newline inside your cell.","title":"Editing cells"},{"location":"enter-data/#copying-and-pasting","text":"You can copy data from Grist or paste data into it. If the pasted range is longer than the available records, new records will be added. Note that Grist does not create new columns automatically. If the pasted data has more columns than the grid displays, extra columns will be omitted.","title":"Copying and pasting"},{"location":"enter-data/#data-entry-widgets","text":"In Grist, columns have types. In addition to typing in values, many column types offer specialized widgets for entering data more conveniently. Here are some of the most useful ones: Toggle . A \u201cToggle\u201d column shows True/False values, and can show them as a \u201cCheckbox\u201d or as a \u201cSwitch\u201d widget, which you can select in the column options. You can toggle a value in such a cell by clicking the check mark or the switch, or by hitting Space . Date and DateTime . Hitting Enter on such a cell will open a calendar to pick a date. Choice and Reference . Typing into a cell of one of these types will produce an auto-complete dropdown menu.","title":"Data entry widgets"},{"location":"enter-data/#linking-to-cells","text":"You can make a sharable link to a cell by pressing \u2318 \u21e7 A (Mac) or Ctrl + Shift + A (Windows) while the cell is selected. This option is also available via the row menu as \u201cCopy anchor link.\u201d The link will be placed in your clipboard, ready to paste into an email or an instant messaging app. The link will open only for people with access to the document.","title":"Linking to cells"},{"location":"examples/","text":"More Examples # Here we will post useful examples of what you can do with Grist, sometimes with ready-to-use templates. Slicing and Dicing Credit Card Expenses : Grist offers a handy way to explore your credit card transactions quickly. American Express card members can use the ready-made template in the example. Book Lists with Library and Store Look-ups : Grist\u2019s link handling and formulas can help you with your book club, by adding automatic links to libraries, stores, review sites, and other references. The same ideas could work with Ikea furniture, food orders, whatever you like. Prepare Emails using Formulas : Turn cells into links that open an email program to compose a new message, and prefill its recipients, subject, and more. Preparing Invoices : Preview printable invoices side by side with your client data. Tracking Payroll : Keep track of employee rates, roles, and hours conveniently, with up-to-date reliable payroll summaries, and detailed history. Print Mailing Labels : Print mailing labels easily. A custom widget supporting popular label sizes makes it quick to generate labels and print from Grist. Treasure Hunt : Plan a treasure hunt without losing track. Brainstorm clues, then assemble them into a consistent trail. Map : Show locations listed in a table on a map. Task Management for Teams : This glorified To-Do list \u2013 similar to the one we use internally at Grist labs \u2013 works well to manage a team\u2019s tasks. Lead List : A very simple list of leads, assigned to individuals to follow up, with control of assignments reserved for document owners. Understanding Link Keys : Learn how to create unique links that share limited views of your Grist document. Reference Columns Guide : Mastering Reference Columns in 3 Steps Summary Tables Guide : Master Summary Tables with 2 Concepts, using the example of a timesheet tracker, with detailed account tracking. Auto Time and User Stamps : Learn to create columns that stamp the time or a user\u2019s name to a record when it is created or updated. Access Rules to Restrict Duplicate Records : Learn to how to create a condition in Access Rules to restrict a duplicate record from being created. Creating Proposals & Contracts : Preview printable forms side by side with your project data. Have something to share? # Have your own template to share with the world? Email us at support@getgrist.com , and we may feature it in a future post.","title":"More examples"},{"location":"examples/#more-examples","text":"Here we will post useful examples of what you can do with Grist, sometimes with ready-to-use templates. Slicing and Dicing Credit Card Expenses : Grist offers a handy way to explore your credit card transactions quickly. American Express card members can use the ready-made template in the example. Book Lists with Library and Store Look-ups : Grist\u2019s link handling and formulas can help you with your book club, by adding automatic links to libraries, stores, review sites, and other references. The same ideas could work with Ikea furniture, food orders, whatever you like. Prepare Emails using Formulas : Turn cells into links that open an email program to compose a new message, and prefill its recipients, subject, and more. Preparing Invoices : Preview printable invoices side by side with your client data. Tracking Payroll : Keep track of employee rates, roles, and hours conveniently, with up-to-date reliable payroll summaries, and detailed history. Print Mailing Labels : Print mailing labels easily. A custom widget supporting popular label sizes makes it quick to generate labels and print from Grist. Treasure Hunt : Plan a treasure hunt without losing track. Brainstorm clues, then assemble them into a consistent trail. Map : Show locations listed in a table on a map. Task Management for Teams : This glorified To-Do list \u2013 similar to the one we use internally at Grist labs \u2013 works well to manage a team\u2019s tasks. Lead List : A very simple list of leads, assigned to individuals to follow up, with control of assignments reserved for document owners. Understanding Link Keys : Learn how to create unique links that share limited views of your Grist document. Reference Columns Guide : Mastering Reference Columns in 3 Steps Summary Tables Guide : Master Summary Tables with 2 Concepts, using the example of a timesheet tracker, with detailed account tracking. Auto Time and User Stamps : Learn to create columns that stamp the time or a user\u2019s name to a record when it is created or updated. Access Rules to Restrict Duplicate Records : Learn to how to create a condition in Access Rules to restrict a duplicate record from being created. Creating Proposals & Contracts : Preview printable forms side by side with your project data.","title":"More Examples"},{"location":"examples/#have-something-to-share","text":"Have your own template to share with the world? Email us at support@getgrist.com , and we may feature it in a future post.","title":"Have something to share?"},{"location":"exports/","text":"Exporting # Exporting a table # If you want to export a table to another spreadsheet or database, you can export that table as either an XLSX file or a CSV, a common interchange format for data. To do this, open your document to the desired table or widget. Then click the three dot menu in the top right of the widget. Select either \u201cDownload as CSV\u201d or \u201cDownload as XLSX\u201d. Your browser will then download a file containing a header row naming your columns, excluding any hidden columns or filtered-out rows, followed by all the rows of data visible in the table. Exporting a document # If you want to export all tables to Excel format, click the sharing icon ( ) on the top right of the screen and select \u201cExport XLSX\u201d. Your browser will then download an Excel file, where each table is a separate sheet containing all rows, without any filters applied. To use this option you need to have full read access for all tables in a document. A note about \u201cExport CSV\u201d in the sharing menu. When on a page with multiple page widgets, \u201cExport CSV\u201d will export only the data in the currently-selected widget. To export all your data, use \u201cExport XLSX\u201d or the \u201cDownload\u201d option which is explained below in backing up an entire document . Sending to Google Drive # If you want to export a document to Google Drive as a Google Sheet file, click the sharing icon ( ) on the top right of the screen and select \u201cSend to Google Drive\u201d. Grist will first ask you to log in to your Google Account (or use an account you already signed in) and then for permission to create a file in your Google Drive. Grist will be able to create new files and manage them but will not be able to access any other files in your drive. Once you accept the permission request, Grist will export your document to an Excel file and then save it in your Google Drive as a Google Sheet file. To use this option, you need to have full read access for all tables in a document. Backing up an entire document # Grist makes regular backups of documents automatically, as described in Automatic Backups . You can also make manual backups by saving copies of documents in your Grist account. In addition, Grist documents can be downloaded in their entirety as an SQLite database file with a .grist extension. SQLite is a popular database format. The downloaded file will contain all your tabular data, any attached files within those tables, metadata about your tables, pages, and widgets, and a history of recent modifications of the document. It will not contain information about who the document is shared with. To download a Grist document, click the sharing icon ( ) on the top right of the screen, and select \u201cDownload\u201d. Restoring from backup # A downloaded .grist file can be uploaded again to provide an exact copy of the original. To upload the file, open the team or personal site where you want to place it, and optionally select also a workspace. Then click on \u201cAdd New\u201d in the top left, and select \u201cImport document\u201d. You may also import CSV and Excel files as new Grist documents this way.","title":"Exports & backups"},{"location":"exports/#exporting","text":"","title":"Exporting"},{"location":"exports/#exporting-a-table","text":"If you want to export a table to another spreadsheet or database, you can export that table as either an XLSX file or a CSV, a common interchange format for data. To do this, open your document to the desired table or widget. Then click the three dot menu in the top right of the widget. Select either \u201cDownload as CSV\u201d or \u201cDownload as XLSX\u201d. Your browser will then download a file containing a header row naming your columns, excluding any hidden columns or filtered-out rows, followed by all the rows of data visible in the table.","title":"Exporting a table"},{"location":"exports/#exporting-a-document","text":"If you want to export all tables to Excel format, click the sharing icon ( ) on the top right of the screen and select \u201cExport XLSX\u201d. Your browser will then download an Excel file, where each table is a separate sheet containing all rows, without any filters applied. To use this option you need to have full read access for all tables in a document. A note about \u201cExport CSV\u201d in the sharing menu. When on a page with multiple page widgets, \u201cExport CSV\u201d will export only the data in the currently-selected widget. To export all your data, use \u201cExport XLSX\u201d or the \u201cDownload\u201d option which is explained below in backing up an entire document .","title":"Exporting a document"},{"location":"exports/#sending-to-google-drive","text":"If you want to export a document to Google Drive as a Google Sheet file, click the sharing icon ( ) on the top right of the screen and select \u201cSend to Google Drive\u201d. Grist will first ask you to log in to your Google Account (or use an account you already signed in) and then for permission to create a file in your Google Drive. Grist will be able to create new files and manage them but will not be able to access any other files in your drive. Once you accept the permission request, Grist will export your document to an Excel file and then save it in your Google Drive as a Google Sheet file. To use this option, you need to have full read access for all tables in a document.","title":"Sending to Google Drive"},{"location":"exports/#backing-up-an-entire-document","text":"Grist makes regular backups of documents automatically, as described in Automatic Backups . You can also make manual backups by saving copies of documents in your Grist account. In addition, Grist documents can be downloaded in their entirety as an SQLite database file with a .grist extension. SQLite is a popular database format. The downloaded file will contain all your tabular data, any attached files within those tables, metadata about your tables, pages, and widgets, and a history of recent modifications of the document. It will not contain information about who the document is shared with. To download a Grist document, click the sharing icon ( ) on the top right of the screen, and select \u201cDownload\u201d.","title":"Backing up an entire document"},{"location":"exports/#restoring-from-backup","text":"A downloaded .grist file can be uploaded again to provide an exact copy of the original. To upload the file, open the team or personal site where you want to place it, and optionally select also a workspace. Then click on \u201cAdd New\u201d in the top left, and select \u201cImport document\u201d. You may also import CSV and Excel files as new Grist documents this way.","title":"Restoring from backup"},{"location":"formula-cheat-sheet/","text":"Formula Cheat Sheet # Grist has a powerful data engine to calculate the cells of your tables using formulas. Grist formulas are written in Python , the most popular language for data science. We also have a suite of Excel-like functions , with all-uppercase names. Here are some helpful notes: Formulas apply to the entire column Fields are included in formulas as $ColumnID . Python is case-sensitive, including for Grist table and column names. If your column ID is title , the formula will use $title , where both are lowercase. You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. If you don\u2019t see what you\u2019re looking for, post in the Community Forum and we\u2019ll be able to help you out! Math Functions # Simple Math (add, subtract, multiply divide) # Uses + , - , / and * operators to complete calculations. Example of Simple Math # Chestwood Art Studio ships art across the country and has the option of monthly payments over the course of 12 months. We have the subtotal, the tax (based on the state it is shipping to) and Amount Due Monthly. This formula column uses addition, multiplication and division. The formula used here is: ($Subtotal + ($Subtotal*$Tax)) / 12 We add the subtotal to the calculated tax then divide this by 12 months to get our Amount Due Monthly. Troubleshooting Errors # #TypeError : Confirm all columns used in the formula are of Numeric type. max and min # Allows you to find the max or min values in a list. Examples using MAX() and MIN() # MAX() and MIN() when capitalized are spreadsheet functions which require a specific syntax. Spreadsheet formula syntax is summarized in our functions reference . max() and min() in lowercase are Python functions. Max : Classes table of the Class Enrollment template. The formula used in the \u2018Spots Left\u2019 column of the Classes table is: max($Max_Students - $Count, 0) or \"Full\" This formula shows the number of spots remaining in a class, or the text \u2018Full\u2019 when the class is full or oversubscribed. We build a list between the parenthesis consisting of two items: $Max_Students - $Count and 0 . The formula returns whichever is greater. When $Count is less than $Max_Students , the difference $Max_Students - $Count is positive and represents the spots left in the class. When $Count exceeds $Max_Students , then the class is full or oversubscribed, and $Max_Students - $Count is negative. The maximum of a negative number and 0 will be 0, so max($Max_Students - $Count, 0) is 0. This represents a full class. The addition of or \"Full\" is applied when the value is falsy, which means that a 0 is replaced with the text \"Full\" . Min : Contacts table of the Lightweight CRM template. The formula used in the \u2018Due\u2019 column of the Contacts table is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Let\u2019s break this down. Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") finds all records in the Interactions table where the Contacts match and the Type is To-Do. This returns a list of records that we assign to the variable items . Next, we use dot notation to find all Dates assigned to the records in our items list. These dates are evaluated to find the minimum date. This is the value that is returned. So, we see the date of the task that is due the soonest. If there are no items in the list, nothing is returned and the field is left blank. In the MAX() example, the list has two items: $Max_Students - $Count and 0 , and the formula returns whichever is greater. In the min() example, the variable items is pulling a list of records based on the lookupRecords arguments, listing the dates, and returning the smallest date. Note that this is a Python function. If we had written the formula as MIN(), a spreadsheet function, the formula would not work because the spreadsheet formula requires a very specific format . Sum # Use the SUM() function when you want to sum a list of values available within a cell. If you want to sum values in a column, use Summary Tables . Example of SUM() # Custom Product Builder template The formula used in the Total Cost column of the Select or Add New Products table is: SUM($Requirements.Cost) The Requirements column is a hidden column in this table. It is a reference list column that pulls data from the Build Requirements table. Our formula uses the Requirements column to access the Build Requirements table then pulls the cost for each record in the table. We use SUM() to sum the costs from each record. Inventory Manager template The formula used in the Received column of the All Products table is: SUM(Incoming_Order_Line_Items.lookupRecords(SKU=$id).Received_Qty) We use the lookupRecords function to find all records in the Incoming Order Line Items table where the SKU matches the SKU in this row then pull the value in the Received Qty column for each of those records. We use SUM() to find the sum of those values. The Qty on Order and Sold columns of the All Products table are also great examples of the SUM() function. Check out another example in our Community Forum: Creating a Sum of Net and Gross profit from multiple tables Comparing for equality: == and != # When comparing for equality in Python, we use == for \u2018equals\u2019 and != for \u2018does not equal\u2019. If $A is 2 and $B is 3, the formula $A == $B would return False , while the formula $A != $B would be True . Examples using == # Inventory Manager template The formula used in the Received Qty column of the Incoming Order Line Items table is: if $Order.Status =='Received': return $Qty else: return None The Order column of the Incoming Order Line Items Table is a reference column that points to the Order Number column of the Incoming Orders table. $Order.Status uses dot notation to pull the value from the Status column of the Incoming Orders table. If the value in this column is equal to Received , the value from the Qty column will be returned. If the value is not equal to Received , nothing is returned. The formula used in the Date Received column of the Create New Order table is: if $Status == \"Received\": return NOW() This is a trigger formula that is triggered when a change is made to the Status column. If the value in the Status column is equal to Received , the current date is returned. If the values are not equal, nothing is returned. Examples using != # Project Management template The formula used in the Missed Deadline column of the Missed Deadline table is: TODAY()> $Due_Date and $Status != \"Completed\" If the current date is greater than the date given in the Due Date column and the value in the Status column is not equal to Completed , the formula is True . If either of these statements is false, the formula is False . Comparing Values: < , > , <= , >= # Allows you to compare numerical values. If Sales is equal to 1200 and Running_Cost is equal to 1650 , \"Gains\" if $Sales > $Running_Cost else \"Loss\" would return Loss . Examples comparing values # Inventory Manager template The formula used in the Stock Alert column of the All Products table is: if $In_Stock + $QTY_on_Order > 5: return \"In Stock\" if $In_Stock + $QTY_on_Order > 0: return \"Low Stock\" else: return \"OUT OF STOCK\" Here, we have two different if-return statements; if x is true, return some_value . Once a statement is true and a value is returned, the formula stops. If both are false, OUT OF STOCK is returned. First, if the value in the In Stock column plus the value in the Qty On Order column are greater than 5, return \u201cIn Stock\u201d. Next, if the value in the In Stock column plus the value in the Qty On Order column are greater than 0, return \u201cLow Stock\u201d. It\u2019s implied that the value is less than or equal to 5 because the first statement would have to be false for this to be evaluated. Last, if all statements are false, return \u201cOUT OF STOCK\u201d. Internal Links Tracker for SEO template The formula used in the Orphaned? column of the Orphaned Pages table is: len(Links.lookupRecords(To=$id))<1 We use the lookupRecords function to find all records in the Links table where the link in the To column matches the link listed in the Slug column of this row. We use len() to count the number of records found. If it\u2019s less than 1, the formula is evaluated to be true and the checkbox will be checked. If it\u2019s equal to or greater than 1, the formula is evaluated to be false. Converting from String to Float # String : A sequence of characters or snippets of text. In code, strings are quoted e.g. 'Hello' or \"-12\" (those are three characters in quotes, as opposed to a negative number). See Python str() Function for converting a specified value to a string. Float : Real numbers that can store decimal values. Also called floating point number. See Python float() Function for converting a specified value into a floating point number. Integer : A whole number, without decimals. See Python int() Function for converting a specified value to an integer number. Example converting a string to a float # Artwork Orders The formula used in the Sale Price column is: if $Appraisal_Value.endswith(\"k\"): return float($Appraisal_Value.rstrip(\"k\")) * 1000 return float($Appraisal_Value) In this example, the Appraisal Value column is a text column that contains alpha-numeric characters. In order to use this value in mathematical formulas, we need to convert from string to float. If the value in the Appraisal Value column ends with \u201ck\u201d, we first use rstrip() to strip \u201ck\u201d from the string in the Appraisal Value column. Now that we only have numeric characters, we use float() to convert our string to a float. Because K represents 1000 and we have removed this from the value, we multiply our float by 1000. If the value in the Appraisal Value column does not end with \u201ck\u201d, and only contains numeric characters, we can simply use float() to convert our string to a float. Troubleshooting # if you are trying to use different columns with numeric values in a mathematical formula but seeing an error, check the column types for each of the columns used in the formula. All need to be of type Numeric . float() is only needed when dealing with alpha-numeric values like we see in the example . TypeError: can\u2019t multiply sequence by non-int of type \u2018float\u2019 This error occurs when a formula attempts to multiply values from multiple columns, at least one of which is not a Numeric type column. In the screenshot below, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for /: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to divide values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the \u2018# of Payments\u2019 column is a Choice column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for +: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to add values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for -: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to subtract values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Discount column is a Text column. When we change the column type to Numeric , the error is resolved. Rounding Numbers # Specify the number of decimal places to give in a result using the ROUND() function. If Average Temperature is equal to 46.5 , ROUND($Average_Temperature) would return 47 Example of rounding numbers # Payroll template The formula used in the Payment column is: ROUND($Hours*$Per_Hour, 2) The ROUND() function follows the format ROUND(value, places) which will round the given value to the number of places specified. Our formula finds the value for $Hours*$Per_Hour then rounds this value to 2 decimal places. Mixing Products The formula used in the Rounded Value column is: mix_list_str = $Mix_Product.Lt_per_100_Lt mix_list_float = [float(i) for i in mix_list_str] x = [Lt * $Water/100 for Lt in mix_list_float] round_x = [ROUND(num, 2) for num in x] l = $Mix_Product.Product ' '.join('{} {}'.format(first, second) for first, second in zip(l, round_x)) Let\u2019s break this down. $Mix_Product represents the Mix Product column, a reference list column that pulls data from the Product column of Table1. We can use this column as a link to Table1 to pull other data. $Mix_Product.Lt_per_100_Lt uses the reference list column, Mix Product, to pull values from the Lt per 100 Lt column of Table1 for the products listed in the Mix Product column of Table2 then assigns this list of values to the variable mix_list_str . This is the same formula used in the Lt per 100 Lt column of Table2 so you can see the value it returns in row 1 of Table2. It returns a list: ['0.2355', '1.2579'] . This list is evaluated as a string rather than numerical values. We need to convert each value in this list to a float. In our next formula, [float(i) for i in mix_list_str] , we iterate through the list that was assigned in the first equation to mix_list_str and convert each value to a floating-point number. We want to convert to a float rather than integer because not all values are whole numbers and contain decimals. i is a variable representing each value. So each value in mix_list_str is evaluated in the equation float(i) . float(0.2355) converts 0.2355 to a float and float(1.2579) converts 1.2579 to a float. Now, we assign our list of floats to the variable mix_list_float . We can now use our float values in a mathematical equation. Once again, we iterate through the list that was assigned to the variable mix_list_float . In our equation [Lt * $Water/100 for Lt in mix_list_float] , Lt represents each value in mix_list_float and $Water represents the value found in the Water column which is 1000 . We evaluate the equation Lt * 1000/100 when Lt = 0.2355 and Lt = 1.2579 which returns the list [2.355, 12.579] . We assign this list to the variable x . To round the values in x to two decimal places, we need to evaluate the equation ROUND(num, 2) where num represents each value in our list and 2 specifies the number of decimal places we want to round to. This returns the list [2.36, 12.58] which we assign to the variable round_x. In the first equation, we used our reference list column, Mix Product, as our link to Table1 in order to pull data from Table1 into Table2. We use this method again in $Mix_Product.Product to pull data from the Product column of Table1. This returns a list of products; [Prod A, Prod B] . We assign this list to the variable l . Finally, we use the join() method to combine our two lists. ' ' is our starting (empty) string. We use Python\u2019s format method to format our string. {} is a placeholder for each variable listed in .format() . Last, we use Python\u2019s zip() function to pair the first values from each list together and then pair the second values in each list together. l is assigned as our first list and round_x is assigned as our second list. l = [Prod A, Prod B] and round_x = [2.36, 12.58] . Zipping our lists into '{} {}'.format(first, second) gives us Prod A 2.36 in our first iteration and Prod B 12.58 in our second iteration. Our final return value is Prod A 2.36 Prod B 12.58 . Formatting numbers with leading zeros # Allows you to specify the minimum number of digits returned in a numerical column by adding leading zeros. If x = 5, str(x).zfill(3) or '{:0>3}'.format(x) would return 005 . Formatting numbers with leading zeros # Community Example: Using Row ID The formula used in the 5-digit ID column of the ID Examples table is: 'TCH{:0>5}'.format($id) '{:0>5}'.format($id) takes the unique row ID and formats it to be a minimum of 5 digits. We then add this to our string \"TCH\" to get our final value. If the $id is longer than 5 digits, the formula returns the string unmodified. We can do the same thing using the str.zfill() method. The formula used in the zfill Method column of the ID Examples table is: str($id).zfill(5) str($id) converts the row ID to a string. .zfill(x) returns a copy of the string with leading zeros to make a string of length x . In our example, it adds leading zeros to make the string 5 characters in length. Again, if the $id is longer than 5 digits, the formula returns the string unmodified. Troubleshooting Errors # #TypeError : can only concatenate str (not \u201cint\u201d) to str If you mean to combine a string and a numerical value, be sure to convert it to string using str() . Working with Strings # Combining Text From Multiple Columns # Method 1: If you have a First Name column and a Last Name column, you can combine these columns to have a Full Name column. If First Name is George and Last Name is Washington , $First_Name + \" \" + $Last_Name would return George Washington . Method 2: If you have additional formatting, an easier way to do this would be using Python\u2019s String format() method . The format() method formats the specified value(s) and inserts them in place of the placeholder, {} . Using the same example as above, our formula would be \"{} {}\".format($First_Name, $Last_Name) . Note: You can click on columns to insert them into your formulas, rather than typing them in. Examples using Method 1 # Class Enrollment template{:target=\u201d_blank\u201d} The formula used in the Full Name column of the Students table is: $Last_Name + \", \" + $First_Name Here, we are combining the value found in the Last Name column with a comma followed by a space followed by the value from the First Name column. When adding any extra characters or spaces, place these between double quotes, as we did in the example with \", \" . An alternative combination of these columns for Full Name could be $First_Name + \" \" + $Last_Name . For the example in row 1, First Name is Brockie and Last Name is Raddon so the value returned would be Brockie Raddon . Inventory Manager template The formula used in the SKU column of the All Products table is: $Brand.Brand_Code+\"-\"+$Color.Code+\"-\"+$Size Brand is a reference column that pulls data from the Name Brand column of the Add Products table. We use this reference column in $Brand.Brand_Code to pull data from the Brand Code column of the Add Products table. Color is a reference column that pulls data from the Color column of the Color table. We use this reference column in $Color.Code to pull data from the Code column of the Color table. Each of the values found in $Brand.Brand_Code and $Color.Code are combined with the value in the Size column with a - between each of the three values to make up the SKU. Examples using Method 2 # Tracking Time + Invoicing template The formula used in the Project Name column of the Projects table is: \"{}: {}\".format($Client.Name, $Name) Let\u2019s break this down. Everything between double quotes \" is our string. The curly brackets {} are placeholders for the values found using .format() which is Python\u2019s string format() method. The first set of curly brackets are replaced with the value found in $Client.Name . Client is a reference column that pulls data for a specific record from the Clients table. $Client.Name is using our reference column, Client to pull data from the Name column of the Clients table. The second set of curly brackets are replaced with the value found in the Name column of this table. Although the Client column shows the value that we want, we can\u2019t use $Client like we did $Name . This is because the Client column is a reference column. It is referencing the entire record but uses the value from the Name column of the Clients table as a visual representation of that record. Under the column configuration panel on the right hand side, we can change what column value we see for the record. In the screenshot below, \u2018Show Column\u2019 was changed from Name to Email. It doesn\u2019t change the data, it just changes the label on that data in the Client column. It\u2019s still pointing to the same record but now shows a different label. $Client.Name pulls the Name for the record that is referenced in the Client column, regardless of the label we see. Custom Product Builder template The formula used in the All Components column of the CONTRACT_BUILDER Card is: '\\n'.join(sorted( \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() )) We are using the join() method , sorted() function and format() method method all in one! '\\n'.join() adds a new line between each item in the list. sorted() sorts the items in the list alphabetically. This leaves us with the following: \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() We\u2019ll work through this backwards. First, we need to take a look at the Components column which is a hidden column in the All Contracts table. This column is a list of components and their associated quantities for the contract. In the for loop, we assign each item in the list of components two variables, comp and quantity. For Components[3]: 6.0 , comp = Components[3] and quantity = 6.0 . Components[#] specifies a Component in the Components table by Row ID. Components[3] is the component assigned 3 as it\u2019s row id. Now, we run each item from the list above through the equation \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) . comp.Component replaces the first set of {} . comp is the variable with our component ID so comp.Component finds the value in the Component column associated with that row ID. For Components[3] , comp.Component is Nozzle. quantity replaces the second set of {} . Again, the quantity is the second variable in our list. For Components[3]: 6.0 , quantity is 6.0 . Our second set of {} are not empty. They include :g *. This converts the value to a floating-point number. comp.Unit replaces the last set of {} . comp is the variable with our component ID so comp.Unit finds the value in the Unit column associated with that row ID. For Components[3] , comp.Unit is None . *Note that {:g} formats floating point numbers in a particular way that omit decimals when they aren\u2019t needed. There are many options available within placeholders for formatting numbers, dates, etc. Learn more about placeholders here . Email Contacts template The formula used in the Body column of the Advanced Compose table is: \"Dear %s,\\n\\nWelcome to the %s team!\" % ($Contact_Name_as_Plaintext, $Team) This technique uses the % operator instead of the format() method. Format specifiers begin with % followed by a character that represents the data type. %s is a placeholder for a string. The first %s is replaced with the value found in the \u201cContact Name as Plaintext\u201d column which is a hidden column and the second %s is replaced by the value in the Team column. \\n adds a new line. Splitting Strings of Text # Split a string using Python\u2019s split() method. If Full Name is George Washington , $Full_Name.split(\" \") would return [George, Washington] . Example of Splitting Strings of Text # Community Example: Colors The formula in the \u201cColor Reference (Just URL)\u201d column of Table 2 is: split = $Color_Reference.Color.split(\" \") return split[-1] $Color_Reference.Color uses the reference column, \u201cColor Reference\u201d to pull data from the table it is referencing, Table 1. Specifically, it pulls the value from the Color column of Table 1. Color is a text column that contains a hyperlink with a label. We only see the label in Table 1 but as you can see in the screenshot above, the value in the \u2018pink\u2019 cell is expanded to show the entire string which contains \u201cpink\u201d followed by the URL. You can also see this in the \u201cColor Reference\u201d column of Table 2. We want to get the link by itself in \u201cColor Reference (Just URL)\u201d. We can do this using Python\u2019s split() method. .split(\" \") allows us to split the string anywhere there is a space (\" \") . In the Color column, there is a label followed by a space followed by the URL. The value from the Color column is split into a list containing two items Label and URL . This list is assigned to the variable split . We want to return the last item in the list split in order to get our URL . The last item in a list always has index [-1] . return split[-1] returns the last item in the list split . Direct Link to Gmail History for a Contact # If you store contacts in Grist, and use Gmail to email them, you can create a formula that will open Gmail to a list of conversations with that contact. Read about it in the Community: Pull up Gmail history for a particular contact Troubleshooting # Is your URL still showing after you added a label? Make sure your Column Type is Text and Cell Format is Hyperlink. Joining a List of Strings # When you want to join a list of strings, you can use Python\u2019s join() method . Example of Joining a List # Community Example: .join() Example The formula used in the Advertisement column of the 2022 Grand Openings table is: \"Coming soon to a city near you!\\n\" + \" : \".join($New_Location_s_in_2022) Here, we are joining multiple strings to create our advertisement. \"Coming soon to a city near you!\\n\" is returned almost exactly as we see it, minus the quotes \"\" and \\n at the end of the string. The quotes \"\" specify that this is a string and \\n is actually a newline character that can be used to specify a new line within a string. \" : \".join($New_Location_s_in_2022) is also a string but uses Python\u2019s join() method to join values from our choice list column, \u201cNew Locations in 2022\u201d. What we see in quotes before .join is what will separate each value in our list. In this example, each value is separated by a space, : and another space. Finding Duplicates # You can find duplicates in a column using either conditional formatting or a helper column. Read about it in the Community: Ensure unique values or detect duplicates Example of Finding Duplicates # Community Example: Finding Duplicates The formula used in the Duplicate? column of the Duplicates table is: len(Duplicates.lookupRecords(Grocery_List=$Grocery_List))>1 Let\u2019s break this down, working from the inside > out. Duplicates.lookupRecords(Grocery_List=$Grocery_List) This is a lookupRecords function that follows the format of: [Table_Name].lookupRecords([A]=$[B]) Where [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. This formula looks up records in the Duplicates table where a record in the Grocery List column matches another record in the same column. len() counts the number of records in our list. Since each duplicate will match with the other, it should appear twice in our list. This is why len() > 1 . if len() > 1 , the formula is true. If len() <= 1 , the formula is false. This same formula can be used in conditional formatting. This can be seen in the \u2018Grocery List\u2019 column of the Duplicates table. If len() > 1 , our formula is true and the conditional cell color is applied to these cells. If len() <= 1 , our formula is false and the cell color is unchanged. Using a Record\u2019s Unique Identifier in Formulas # When a record is created, it is assigned a numeric id (available as $id in formulas) that is unique within that table. You can reveal the row ID by adding a formula column where the formula is $id . Examples Using Row ID in Formulas # You can reveal the ID with the formula $id Custom Product Builder template The formula used in the Contract No. column of the Contract Builder table is: $id + 500 Here, we are using a trigger formula to create a unique Contract Number when a record is created. Class Enrollment template The formula used in the Count column of the Classes table is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This is refered to as a Reverse Lookup. We can use the row id to match a record in another table where a reference column is used. LookupRecords follows the format [Table_Name].lookupRecords([A]=$[B]) . [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Lookup Records creates a list of records that match the criteria listed. len() counts how many records are in that list. Here, we are looking up records from the Enrollments table where the record called out in the Class column (our reference column) has the same row ID as the row in the table you are entering the formula. Additionally, the value in the Status column of the Enrollments table is Confirmed . We\u2019ll walk through this. The table we are looking up records in is the Enrollments table. Our criteria comes from the Class column and the Status column.The criteria for Status is straightforward; the value must be Confirmed in order to be included in our list of records. Class is a bit more complicated. As we see in the screenshot below, Class is a reference column that pulls data from the Classes table. Here, the Class column shows 2018F-Stars . A reference column points to the entire record, not just the value you see here in the Class column. Using the configuration panel on the right hand side of the screen, you can pick any column from the originating table to show. For this example, the Class column shows the value from the Class Code column of the Classes table but it points to the entire record where the class code is 2018F-Stars . As you can see in this screenshot, the Row ID for this particular record is 2 and because we are calculating the Count for the row with Row ID = 2 , it will count all records in the Enrollment table where Class shows 2018F-Stars and Status is Confirmed . Restaurant Custom Orders template The trigger formula used in the BOM # column of the Bill of Materials table is: MAX(o.BOM_ for o in Bill_Of_Materials.all if o.id != $id) + 1 First, we\u2019ll walk through the formula inside the parenthesis then work outwards. Here, o is a variable representing each record in our table. o.BOM_ represents the BOM # for each record and o.id represents the row ID for each record. This is a for loop that makes a list of the BOM # for each record in the table Bill of Materials when the record ID does not equal the ID of this row. MAX() finds the maximum BOM # in the list then + 1 to get our final value. This is a trigger formula that only applies to new records. When a new record is created, the formula finds the highest BOM # in the table then adds 1 so we have a unique BOM # for the new record. Removing Duplicates From a List # You can remove duplicates from a list with help from Python\u2019s set() method. Example of Removing Duplicates from a List # Community Example: Removing Duplicates From a List The formula in the All Divisions column of the Abroad Trips table is: confirmed_div = $Attending_Confirmed.Role_Division.Division pending_div = $Attending_Pending.Role_Division.Division full_list = confirmed_div + pending_div return sorted(set(full_list)) We will walk through this one line at a time. Attending-Confirmed is a Reference List column that pulls data from the EMPLOYEES table. $Attending_Confirmed.Role_Division pulls the value from the Role Division column of the EMPLOYEES table. The Role Division column in the EMPLOYEES table is a reference column itself, which points to a record in the Divisions table. Chaining allows us to specify what information we want from this record. In this case, we want the Division. We expand our formula to $Attending_Confirmed.Role_Division.Division . The Division is found for each employee listed in the Attending-Confirmed column, creating a list. We assign this list of divisions to the variable confirmed_div . Attending-Pending is also a Reference List column that pulls data from the EMPLOYEES table. $Attending_Pending.Role_Division.Division does the same as above except now we pull the division for each employee in the Attending-Pending column. We assign this list to the variable pending_div . We create a list of all divisions by adding the two lists together and assigning this combined list to the variable full_list . return sorted(set(full_list)) first uses Python\u2019s set() method to create a list with no duplicate divisions. We then use the sorted() method to return our set of divisions, sorted alphabetically. Note that the formula above can be simplified even further to: sorted( set($Attending_Confirmed.Role_Division.Division) | set($Attending_Pending.Role_Division.Division) ) Setting Default Values for New Records # You can set default values for when a new record is created and save yourself the trouble of having to fill in the same fields with the same values time after time. Read about it in the Community: Default values on the widget Working with dates and times # Automatic Date, Time and Author Stamps # You can automatically add the date or time a record was created or updated as well as who made the change. Examples of Automatic Date, Time and Author Stamps # Grant Application Tracker template The formula used in the Last Updated column of the Tasks table is: NOW() This is a trigger formula that triggers when a change is made to any field for this record. When a change is made, this formula runs its calculation. NOW() calculates the current time and date for the time zone selected. The formula used in the Created By column of the Tasks table is: user.Name This is a trigger formula that triggers when a new record is created. When the record is created, this formula runs its calculation. user.Name looks up the user account that is logged into Grist and returns the name associated with that account. Troubleshooting Errors # If the time value in your datetime column is not calculating, check your formula. If TODAY() is used in DateTime, the time will always show 12:00am as you see below. NOW() is used for DateTime columns. TODAY() is used for Date. #AttributeError You have likely entered user.name but the formula is user.Name . Keep an eye on capitalization! #NameError You may have entered username or userName . The correct formula is user.Name . Another possibility is that this was entered in as a Formula column rather than a trigger formula column. Convert it to a trigger formula and this should resolve the problem. Filtering Data within a Specified Amount of Time # Using the DATEADD() function and comparision operators , you can determine if a date falls within a specific range then apply a filter. Example Filtering Data that \u2018Falls in 1 Month Range` # Community Example: Filtering Data Within a 1-Month Range The formula used in the Falls in 1 Month Range? column of the Interactions table is: TODAY() >= $Date >= DATEADD(TODAY(),months=-1) TODAY() returns the current date. $Date is the name of a column in our table, which is a Date type column. DATEADD(start_date, days=0, months=0, years=0, weeks=0) returns the date that is the given number of days, months, years, or weeks before or after the start_date . In this example, it returns the date that is one month prior to the start date, TODAY() . This formula is true if the date value in the Date column falls between TODAY() and our DATEADD() date which is one month ago. If the date value in the Date column does not fall between these two dates, the formula returns false. We can use this column to filter our data. If we only want to see interactions that fall within the 1 Month Range, we would filter to only include true values. If we want to see interactions that fall outside of the 1 Month Range, we would filter to only include false values. Troubleshooting Errors # #TypeError : Because $Date is a Date type column, TODAY() must be used in formulas comparing dates. NOW() is a DateTime formula that should only be used with other DateTime values. For example, if the $Date column was a DateTime type column, NOW() would need to be used rather than TODAY() because it includes the time component. NOW() is date and time. TODAY() is only date.","title":"Formula Cheat Sheet"},{"location":"formula-cheat-sheet/#formula-cheat-sheet","text":"Grist has a powerful data engine to calculate the cells of your tables using formulas. Grist formulas are written in Python , the most popular language for data science. We also have a suite of Excel-like functions , with all-uppercase names. Here are some helpful notes: Formulas apply to the entire column Fields are included in formulas as $ColumnID . Python is case-sensitive, including for Grist table and column names. If your column ID is title , the formula will use $title , where both are lowercase. You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. If you don\u2019t see what you\u2019re looking for, post in the Community Forum and we\u2019ll be able to help you out!","title":"Formula Cheat Sheet"},{"location":"formula-cheat-sheet/#math-functions","text":"","title":"Math Functions"},{"location":"formula-cheat-sheet/#simple-math-add-subtract-multiply-divide","text":"Uses + , - , / and * operators to complete calculations.","title":"Simple Math (add, subtract, multiply divide)"},{"location":"formula-cheat-sheet/#example-of-simple-math","text":"Chestwood Art Studio ships art across the country and has the option of monthly payments over the course of 12 months. We have the subtotal, the tax (based on the state it is shipping to) and Amount Due Monthly. This formula column uses addition, multiplication and division. The formula used here is: ($Subtotal + ($Subtotal*$Tax)) / 12 We add the subtotal to the calculated tax then divide this by 12 months to get our Amount Due Monthly.","title":"Example of Simple Math"},{"location":"formula-cheat-sheet/#troubleshooting-errors","text":"#TypeError : Confirm all columns used in the formula are of Numeric type.","title":"Troubleshooting Errors"},{"location":"formula-cheat-sheet/#max-and-min","text":"Allows you to find the max or min values in a list.","title":"max and min"},{"location":"formula-cheat-sheet/#examples-using-max-and-min","text":"MAX() and MIN() when capitalized are spreadsheet functions which require a specific syntax. Spreadsheet formula syntax is summarized in our functions reference . max() and min() in lowercase are Python functions. Max : Classes table of the Class Enrollment template. The formula used in the \u2018Spots Left\u2019 column of the Classes table is: max($Max_Students - $Count, 0) or \"Full\" This formula shows the number of spots remaining in a class, or the text \u2018Full\u2019 when the class is full or oversubscribed. We build a list between the parenthesis consisting of two items: $Max_Students - $Count and 0 . The formula returns whichever is greater. When $Count is less than $Max_Students , the difference $Max_Students - $Count is positive and represents the spots left in the class. When $Count exceeds $Max_Students , then the class is full or oversubscribed, and $Max_Students - $Count is negative. The maximum of a negative number and 0 will be 0, so max($Max_Students - $Count, 0) is 0. This represents a full class. The addition of or \"Full\" is applied when the value is falsy, which means that a 0 is replaced with the text \"Full\" . Min : Contacts table of the Lightweight CRM template. The formula used in the \u2018Due\u2019 column of the Contacts table is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Let\u2019s break this down. Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") finds all records in the Interactions table where the Contacts match and the Type is To-Do. This returns a list of records that we assign to the variable items . Next, we use dot notation to find all Dates assigned to the records in our items list. These dates are evaluated to find the minimum date. This is the value that is returned. So, we see the date of the task that is due the soonest. If there are no items in the list, nothing is returned and the field is left blank. In the MAX() example, the list has two items: $Max_Students - $Count and 0 , and the formula returns whichever is greater. In the min() example, the variable items is pulling a list of records based on the lookupRecords arguments, listing the dates, and returning the smallest date. Note that this is a Python function. If we had written the formula as MIN(), a spreadsheet function, the formula would not work because the spreadsheet formula requires a very specific format .","title":"Examples using MAX() and MIN()"},{"location":"formula-cheat-sheet/#sum","text":"Use the SUM() function when you want to sum a list of values available within a cell. If you want to sum values in a column, use Summary Tables .","title":"Sum"},{"location":"formula-cheat-sheet/#example-of-sum","text":"Custom Product Builder template The formula used in the Total Cost column of the Select or Add New Products table is: SUM($Requirements.Cost) The Requirements column is a hidden column in this table. It is a reference list column that pulls data from the Build Requirements table. Our formula uses the Requirements column to access the Build Requirements table then pulls the cost for each record in the table. We use SUM() to sum the costs from each record. Inventory Manager template The formula used in the Received column of the All Products table is: SUM(Incoming_Order_Line_Items.lookupRecords(SKU=$id).Received_Qty) We use the lookupRecords function to find all records in the Incoming Order Line Items table where the SKU matches the SKU in this row then pull the value in the Received Qty column for each of those records. We use SUM() to find the sum of those values. The Qty on Order and Sold columns of the All Products table are also great examples of the SUM() function. Check out another example in our Community Forum: Creating a Sum of Net and Gross profit from multiple tables","title":"Example of SUM()"},{"location":"formula-cheat-sheet/#comparing-for-equality-and","text":"When comparing for equality in Python, we use == for \u2018equals\u2019 and != for \u2018does not equal\u2019. If $A is 2 and $B is 3, the formula $A == $B would return False , while the formula $A != $B would be True .","title":"Comparing for equality: == and !="},{"location":"formula-cheat-sheet/#examples-using","text":"Inventory Manager template The formula used in the Received Qty column of the Incoming Order Line Items table is: if $Order.Status =='Received': return $Qty else: return None The Order column of the Incoming Order Line Items Table is a reference column that points to the Order Number column of the Incoming Orders table. $Order.Status uses dot notation to pull the value from the Status column of the Incoming Orders table. If the value in this column is equal to Received , the value from the Qty column will be returned. If the value is not equal to Received , nothing is returned. The formula used in the Date Received column of the Create New Order table is: if $Status == \"Received\": return NOW() This is a trigger formula that is triggered when a change is made to the Status column. If the value in the Status column is equal to Received , the current date is returned. If the values are not equal, nothing is returned.","title":"Examples using =="},{"location":"formula-cheat-sheet/#examples-using_1","text":"Project Management template The formula used in the Missed Deadline column of the Missed Deadline table is: TODAY()> $Due_Date and $Status != \"Completed\" If the current date is greater than the date given in the Due Date column and the value in the Status column is not equal to Completed , the formula is True . If either of these statements is false, the formula is False .","title":"Examples using !="},{"location":"formula-cheat-sheet/#comparing-values","text":"Allows you to compare numerical values. If Sales is equal to 1200 and Running_Cost is equal to 1650 , \"Gains\" if $Sales > $Running_Cost else \"Loss\" would return Loss .","title":"Comparing Values: < , > , <= , >="},{"location":"formula-cheat-sheet/#examples-comparing-values","text":"Inventory Manager template The formula used in the Stock Alert column of the All Products table is: if $In_Stock + $QTY_on_Order > 5: return \"In Stock\" if $In_Stock + $QTY_on_Order > 0: return \"Low Stock\" else: return \"OUT OF STOCK\" Here, we have two different if-return statements; if x is true, return some_value . Once a statement is true and a value is returned, the formula stops. If both are false, OUT OF STOCK is returned. First, if the value in the In Stock column plus the value in the Qty On Order column are greater than 5, return \u201cIn Stock\u201d. Next, if the value in the In Stock column plus the value in the Qty On Order column are greater than 0, return \u201cLow Stock\u201d. It\u2019s implied that the value is less than or equal to 5 because the first statement would have to be false for this to be evaluated. Last, if all statements are false, return \u201cOUT OF STOCK\u201d. Internal Links Tracker for SEO template The formula used in the Orphaned? column of the Orphaned Pages table is: len(Links.lookupRecords(To=$id))<1 We use the lookupRecords function to find all records in the Links table where the link in the To column matches the link listed in the Slug column of this row. We use len() to count the number of records found. If it\u2019s less than 1, the formula is evaluated to be true and the checkbox will be checked. If it\u2019s equal to or greater than 1, the formula is evaluated to be false.","title":"Examples comparing values"},{"location":"formula-cheat-sheet/#converting-from-string-to-float","text":"String : A sequence of characters or snippets of text. In code, strings are quoted e.g. 'Hello' or \"-12\" (those are three characters in quotes, as opposed to a negative number). See Python str() Function for converting a specified value to a string. Float : Real numbers that can store decimal values. Also called floating point number. See Python float() Function for converting a specified value into a floating point number. Integer : A whole number, without decimals. See Python int() Function for converting a specified value to an integer number.","title":"Converting from String to Float"},{"location":"formula-cheat-sheet/#example-converting-a-string-to-a-float","text":"Artwork Orders The formula used in the Sale Price column is: if $Appraisal_Value.endswith(\"k\"): return float($Appraisal_Value.rstrip(\"k\")) * 1000 return float($Appraisal_Value) In this example, the Appraisal Value column is a text column that contains alpha-numeric characters. In order to use this value in mathematical formulas, we need to convert from string to float. If the value in the Appraisal Value column ends with \u201ck\u201d, we first use rstrip() to strip \u201ck\u201d from the string in the Appraisal Value column. Now that we only have numeric characters, we use float() to convert our string to a float. Because K represents 1000 and we have removed this from the value, we multiply our float by 1000. If the value in the Appraisal Value column does not end with \u201ck\u201d, and only contains numeric characters, we can simply use float() to convert our string to a float.","title":"Example converting a string to a float"},{"location":"formula-cheat-sheet/#troubleshooting","text":"if you are trying to use different columns with numeric values in a mathematical formula but seeing an error, check the column types for each of the columns used in the formula. All need to be of type Numeric . float() is only needed when dealing with alpha-numeric values like we see in the example . TypeError: can\u2019t multiply sequence by non-int of type \u2018float\u2019 This error occurs when a formula attempts to multiply values from multiple columns, at least one of which is not a Numeric type column. In the screenshot below, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for /: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to divide values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the \u2018# of Payments\u2019 column is a Choice column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for +: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to add values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Tax column is a Text column. When we change the column type to Numeric , the error is resolved. TypeError: unsupported operand type(s) for -: \u2018float\u2019 and \u2018str\u2019 This error occurs when a formula attempts to subtract values from multiple columns, at least one of which is not a Numeric type column. In the example seen above, the Discount column is a Text column. When we change the column type to Numeric , the error is resolved.","title":"Troubleshooting"},{"location":"formula-cheat-sheet/#rounding-numbers","text":"Specify the number of decimal places to give in a result using the ROUND() function. If Average Temperature is equal to 46.5 , ROUND($Average_Temperature) would return 47","title":"Rounding Numbers"},{"location":"formula-cheat-sheet/#example-of-rounding-numbers","text":"Payroll template The formula used in the Payment column is: ROUND($Hours*$Per_Hour, 2) The ROUND() function follows the format ROUND(value, places) which will round the given value to the number of places specified. Our formula finds the value for $Hours*$Per_Hour then rounds this value to 2 decimal places. Mixing Products The formula used in the Rounded Value column is: mix_list_str = $Mix_Product.Lt_per_100_Lt mix_list_float = [float(i) for i in mix_list_str] x = [Lt * $Water/100 for Lt in mix_list_float] round_x = [ROUND(num, 2) for num in x] l = $Mix_Product.Product ' '.join('{} {}'.format(first, second) for first, second in zip(l, round_x)) Let\u2019s break this down. $Mix_Product represents the Mix Product column, a reference list column that pulls data from the Product column of Table1. We can use this column as a link to Table1 to pull other data. $Mix_Product.Lt_per_100_Lt uses the reference list column, Mix Product, to pull values from the Lt per 100 Lt column of Table1 for the products listed in the Mix Product column of Table2 then assigns this list of values to the variable mix_list_str . This is the same formula used in the Lt per 100 Lt column of Table2 so you can see the value it returns in row 1 of Table2. It returns a list: ['0.2355', '1.2579'] . This list is evaluated as a string rather than numerical values. We need to convert each value in this list to a float. In our next formula, [float(i) for i in mix_list_str] , we iterate through the list that was assigned in the first equation to mix_list_str and convert each value to a floating-point number. We want to convert to a float rather than integer because not all values are whole numbers and contain decimals. i is a variable representing each value. So each value in mix_list_str is evaluated in the equation float(i) . float(0.2355) converts 0.2355 to a float and float(1.2579) converts 1.2579 to a float. Now, we assign our list of floats to the variable mix_list_float . We can now use our float values in a mathematical equation. Once again, we iterate through the list that was assigned to the variable mix_list_float . In our equation [Lt * $Water/100 for Lt in mix_list_float] , Lt represents each value in mix_list_float and $Water represents the value found in the Water column which is 1000 . We evaluate the equation Lt * 1000/100 when Lt = 0.2355 and Lt = 1.2579 which returns the list [2.355, 12.579] . We assign this list to the variable x . To round the values in x to two decimal places, we need to evaluate the equation ROUND(num, 2) where num represents each value in our list and 2 specifies the number of decimal places we want to round to. This returns the list [2.36, 12.58] which we assign to the variable round_x. In the first equation, we used our reference list column, Mix Product, as our link to Table1 in order to pull data from Table1 into Table2. We use this method again in $Mix_Product.Product to pull data from the Product column of Table1. This returns a list of products; [Prod A, Prod B] . We assign this list to the variable l . Finally, we use the join() method to combine our two lists. ' ' is our starting (empty) string. We use Python\u2019s format method to format our string. {} is a placeholder for each variable listed in .format() . Last, we use Python\u2019s zip() function to pair the first values from each list together and then pair the second values in each list together. l is assigned as our first list and round_x is assigned as our second list. l = [Prod A, Prod B] and round_x = [2.36, 12.58] . Zipping our lists into '{} {}'.format(first, second) gives us Prod A 2.36 in our first iteration and Prod B 12.58 in our second iteration. Our final return value is Prod A 2.36 Prod B 12.58 .","title":"Example of rounding numbers"},{"location":"formula-cheat-sheet/#formatting-numbers-with-leading-zeros","text":"Allows you to specify the minimum number of digits returned in a numerical column by adding leading zeros. If x = 5, str(x).zfill(3) or '{:0>3}'.format(x) would return 005 .","title":"Formatting numbers with leading zeros"},{"location":"formula-cheat-sheet/#formatting-numbers-with-leading-zeros_1","text":"Community Example: Using Row ID The formula used in the 5-digit ID column of the ID Examples table is: 'TCH{:0>5}'.format($id) '{:0>5}'.format($id) takes the unique row ID and formats it to be a minimum of 5 digits. We then add this to our string \"TCH\" to get our final value. If the $id is longer than 5 digits, the formula returns the string unmodified. We can do the same thing using the str.zfill() method. The formula used in the zfill Method column of the ID Examples table is: str($id).zfill(5) str($id) converts the row ID to a string. .zfill(x) returns a copy of the string with leading zeros to make a string of length x . In our example, it adds leading zeros to make the string 5 characters in length. Again, if the $id is longer than 5 digits, the formula returns the string unmodified.","title":"Formatting numbers with leading zeros"},{"location":"formula-cheat-sheet/#troubleshooting-errors_1","text":"#TypeError : can only concatenate str (not \u201cint\u201d) to str If you mean to combine a string and a numerical value, be sure to convert it to string using str() .","title":"Troubleshooting Errors"},{"location":"formula-cheat-sheet/#working-with-strings","text":"","title":"Working with Strings"},{"location":"formula-cheat-sheet/#combining-text-from-multiple-columns","text":"Method 1: If you have a First Name column and a Last Name column, you can combine these columns to have a Full Name column. If First Name is George and Last Name is Washington , $First_Name + \" \" + $Last_Name would return George Washington . Method 2: If you have additional formatting, an easier way to do this would be using Python\u2019s String format() method . The format() method formats the specified value(s) and inserts them in place of the placeholder, {} . Using the same example as above, our formula would be \"{} {}\".format($First_Name, $Last_Name) . Note: You can click on columns to insert them into your formulas, rather than typing them in.","title":"Combining Text From Multiple Columns"},{"location":"formula-cheat-sheet/#examples-using-method-1","text":"Class Enrollment template{:target=\u201d_blank\u201d} The formula used in the Full Name column of the Students table is: $Last_Name + \", \" + $First_Name Here, we are combining the value found in the Last Name column with a comma followed by a space followed by the value from the First Name column. When adding any extra characters or spaces, place these between double quotes, as we did in the example with \", \" . An alternative combination of these columns for Full Name could be $First_Name + \" \" + $Last_Name . For the example in row 1, First Name is Brockie and Last Name is Raddon so the value returned would be Brockie Raddon . Inventory Manager template The formula used in the SKU column of the All Products table is: $Brand.Brand_Code+\"-\"+$Color.Code+\"-\"+$Size Brand is a reference column that pulls data from the Name Brand column of the Add Products table. We use this reference column in $Brand.Brand_Code to pull data from the Brand Code column of the Add Products table. Color is a reference column that pulls data from the Color column of the Color table. We use this reference column in $Color.Code to pull data from the Code column of the Color table. Each of the values found in $Brand.Brand_Code and $Color.Code are combined with the value in the Size column with a - between each of the three values to make up the SKU.","title":"Examples using Method 1"},{"location":"formula-cheat-sheet/#examples-using-method-2","text":"Tracking Time + Invoicing template The formula used in the Project Name column of the Projects table is: \"{}: {}\".format($Client.Name, $Name) Let\u2019s break this down. Everything between double quotes \" is our string. The curly brackets {} are placeholders for the values found using .format() which is Python\u2019s string format() method. The first set of curly brackets are replaced with the value found in $Client.Name . Client is a reference column that pulls data for a specific record from the Clients table. $Client.Name is using our reference column, Client to pull data from the Name column of the Clients table. The second set of curly brackets are replaced with the value found in the Name column of this table. Although the Client column shows the value that we want, we can\u2019t use $Client like we did $Name . This is because the Client column is a reference column. It is referencing the entire record but uses the value from the Name column of the Clients table as a visual representation of that record. Under the column configuration panel on the right hand side, we can change what column value we see for the record. In the screenshot below, \u2018Show Column\u2019 was changed from Name to Email. It doesn\u2019t change the data, it just changes the label on that data in the Client column. It\u2019s still pointing to the same record but now shows a different label. $Client.Name pulls the Name for the record that is referenced in the Client column, regardless of the label we see. Custom Product Builder template The formula used in the All Components column of the CONTRACT_BUILDER Card is: '\\n'.join(sorted( \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() )) We are using the join() method , sorted() function and format() method method all in one! '\\n'.join() adds a new line between each item in the list. sorted() sorts the items in the list alphabetically. This leaves us with the following: \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) for (comp, quantity) in $Components.items() We\u2019ll work through this backwards. First, we need to take a look at the Components column which is a hidden column in the All Contracts table. This column is a list of components and their associated quantities for the contract. In the for loop, we assign each item in the list of components two variables, comp and quantity. For Components[3]: 6.0 , comp = Components[3] and quantity = 6.0 . Components[#] specifies a Component in the Components table by Row ID. Components[3] is the component assigned 3 as it\u2019s row id. Now, we run each item from the list above through the equation \"{} \u2014 {:g} {}\".format(comp.Component, quantity, comp.Unit) . comp.Component replaces the first set of {} . comp is the variable with our component ID so comp.Component finds the value in the Component column associated with that row ID. For Components[3] , comp.Component is Nozzle. quantity replaces the second set of {} . Again, the quantity is the second variable in our list. For Components[3]: 6.0 , quantity is 6.0 . Our second set of {} are not empty. They include :g *. This converts the value to a floating-point number. comp.Unit replaces the last set of {} . comp is the variable with our component ID so comp.Unit finds the value in the Unit column associated with that row ID. For Components[3] , comp.Unit is None . *Note that {:g} formats floating point numbers in a particular way that omit decimals when they aren\u2019t needed. There are many options available within placeholders for formatting numbers, dates, etc. Learn more about placeholders here . Email Contacts template The formula used in the Body column of the Advanced Compose table is: \"Dear %s,\\n\\nWelcome to the %s team!\" % ($Contact_Name_as_Plaintext, $Team) This technique uses the % operator instead of the format() method. Format specifiers begin with % followed by a character that represents the data type. %s is a placeholder for a string. The first %s is replaced with the value found in the \u201cContact Name as Plaintext\u201d column which is a hidden column and the second %s is replaced by the value in the Team column. \\n adds a new line.","title":"Examples using Method 2"},{"location":"formula-cheat-sheet/#splitting-strings-of-text","text":"Split a string using Python\u2019s split() method. If Full Name is George Washington , $Full_Name.split(\" \") would return [George, Washington] .","title":"Splitting Strings of Text"},{"location":"formula-cheat-sheet/#example-of-splitting-strings-of-text","text":"Community Example: Colors The formula in the \u201cColor Reference (Just URL)\u201d column of Table 2 is: split = $Color_Reference.Color.split(\" \") return split[-1] $Color_Reference.Color uses the reference column, \u201cColor Reference\u201d to pull data from the table it is referencing, Table 1. Specifically, it pulls the value from the Color column of Table 1. Color is a text column that contains a hyperlink with a label. We only see the label in Table 1 but as you can see in the screenshot above, the value in the \u2018pink\u2019 cell is expanded to show the entire string which contains \u201cpink\u201d followed by the URL. You can also see this in the \u201cColor Reference\u201d column of Table 2. We want to get the link by itself in \u201cColor Reference (Just URL)\u201d. We can do this using Python\u2019s split() method. .split(\" \") allows us to split the string anywhere there is a space (\" \") . In the Color column, there is a label followed by a space followed by the URL. The value from the Color column is split into a list containing two items Label and URL . This list is assigned to the variable split . We want to return the last item in the list split in order to get our URL . The last item in a list always has index [-1] . return split[-1] returns the last item in the list split .","title":"Example of Splitting Strings of Text"},{"location":"formula-cheat-sheet/#direct-link-to-gmail-history-for-a-contact","text":"If you store contacts in Grist, and use Gmail to email them, you can create a formula that will open Gmail to a list of conversations with that contact. Read about it in the Community: Pull up Gmail history for a particular contact","title":"Direct Link to Gmail History for a Contact"},{"location":"formula-cheat-sheet/#troubleshooting_1","text":"Is your URL still showing after you added a label? Make sure your Column Type is Text and Cell Format is Hyperlink.","title":"Troubleshooting"},{"location":"formula-cheat-sheet/#joining-a-list-of-strings","text":"When you want to join a list of strings, you can use Python\u2019s join() method .","title":"Joining a List of Strings"},{"location":"formula-cheat-sheet/#example-of-joining-a-list","text":"Community Example: .join() Example The formula used in the Advertisement column of the 2022 Grand Openings table is: \"Coming soon to a city near you!\\n\" + \" : \".join($New_Location_s_in_2022) Here, we are joining multiple strings to create our advertisement. \"Coming soon to a city near you!\\n\" is returned almost exactly as we see it, minus the quotes \"\" and \\n at the end of the string. The quotes \"\" specify that this is a string and \\n is actually a newline character that can be used to specify a new line within a string. \" : \".join($New_Location_s_in_2022) is also a string but uses Python\u2019s join() method to join values from our choice list column, \u201cNew Locations in 2022\u201d. What we see in quotes before .join is what will separate each value in our list. In this example, each value is separated by a space, : and another space.","title":"Example of Joining a List"},{"location":"formula-cheat-sheet/#finding-duplicates","text":"You can find duplicates in a column using either conditional formatting or a helper column. Read about it in the Community: Ensure unique values or detect duplicates","title":"Finding Duplicates"},{"location":"formula-cheat-sheet/#example-of-finding-duplicates","text":"Community Example: Finding Duplicates The formula used in the Duplicate? column of the Duplicates table is: len(Duplicates.lookupRecords(Grocery_List=$Grocery_List))>1 Let\u2019s break this down, working from the inside > out. Duplicates.lookupRecords(Grocery_List=$Grocery_List) This is a lookupRecords function that follows the format of: [Table_Name].lookupRecords([A]=$[B]) Where [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. This formula looks up records in the Duplicates table where a record in the Grocery List column matches another record in the same column. len() counts the number of records in our list. Since each duplicate will match with the other, it should appear twice in our list. This is why len() > 1 . if len() > 1 , the formula is true. If len() <= 1 , the formula is false. This same formula can be used in conditional formatting. This can be seen in the \u2018Grocery List\u2019 column of the Duplicates table. If len() > 1 , our formula is true and the conditional cell color is applied to these cells. If len() <= 1 , our formula is false and the cell color is unchanged.","title":"Example of Finding Duplicates"},{"location":"formula-cheat-sheet/#using-a-records-unique-identifier-in-formulas","text":"When a record is created, it is assigned a numeric id (available as $id in formulas) that is unique within that table. You can reveal the row ID by adding a formula column where the formula is $id .","title":"Using a Record's Unique Identifier in Formulas"},{"location":"formula-cheat-sheet/#examples-using-row-id-in-formulas","text":"You can reveal the ID with the formula $id Custom Product Builder template The formula used in the Contract No. column of the Contract Builder table is: $id + 500 Here, we are using a trigger formula to create a unique Contract Number when a record is created. Class Enrollment template The formula used in the Count column of the Classes table is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This is refered to as a Reverse Lookup. We can use the row id to match a record in another table where a reference column is used. LookupRecords follows the format [Table_Name].lookupRecords([A]=$[B]) . [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Lookup Records creates a list of records that match the criteria listed. len() counts how many records are in that list. Here, we are looking up records from the Enrollments table where the record called out in the Class column (our reference column) has the same row ID as the row in the table you are entering the formula. Additionally, the value in the Status column of the Enrollments table is Confirmed . We\u2019ll walk through this. The table we are looking up records in is the Enrollments table. Our criteria comes from the Class column and the Status column.The criteria for Status is straightforward; the value must be Confirmed in order to be included in our list of records. Class is a bit more complicated. As we see in the screenshot below, Class is a reference column that pulls data from the Classes table. Here, the Class column shows 2018F-Stars . A reference column points to the entire record, not just the value you see here in the Class column. Using the configuration panel on the right hand side of the screen, you can pick any column from the originating table to show. For this example, the Class column shows the value from the Class Code column of the Classes table but it points to the entire record where the class code is 2018F-Stars . As you can see in this screenshot, the Row ID for this particular record is 2 and because we are calculating the Count for the row with Row ID = 2 , it will count all records in the Enrollment table where Class shows 2018F-Stars and Status is Confirmed . Restaurant Custom Orders template The trigger formula used in the BOM # column of the Bill of Materials table is: MAX(o.BOM_ for o in Bill_Of_Materials.all if o.id != $id) + 1 First, we\u2019ll walk through the formula inside the parenthesis then work outwards. Here, o is a variable representing each record in our table. o.BOM_ represents the BOM # for each record and o.id represents the row ID for each record. This is a for loop that makes a list of the BOM # for each record in the table Bill of Materials when the record ID does not equal the ID of this row. MAX() finds the maximum BOM # in the list then + 1 to get our final value. This is a trigger formula that only applies to new records. When a new record is created, the formula finds the highest BOM # in the table then adds 1 so we have a unique BOM # for the new record.","title":"Examples Using Row ID in Formulas"},{"location":"formula-cheat-sheet/#removing-duplicates-from-a-list","text":"You can remove duplicates from a list with help from Python\u2019s set() method.","title":"Removing Duplicates From a List"},{"location":"formula-cheat-sheet/#example-of-removing-duplicates-from-a-list","text":"Community Example: Removing Duplicates From a List The formula in the All Divisions column of the Abroad Trips table is: confirmed_div = $Attending_Confirmed.Role_Division.Division pending_div = $Attending_Pending.Role_Division.Division full_list = confirmed_div + pending_div return sorted(set(full_list)) We will walk through this one line at a time. Attending-Confirmed is a Reference List column that pulls data from the EMPLOYEES table. $Attending_Confirmed.Role_Division pulls the value from the Role Division column of the EMPLOYEES table. The Role Division column in the EMPLOYEES table is a reference column itself, which points to a record in the Divisions table. Chaining allows us to specify what information we want from this record. In this case, we want the Division. We expand our formula to $Attending_Confirmed.Role_Division.Division . The Division is found for each employee listed in the Attending-Confirmed column, creating a list. We assign this list of divisions to the variable confirmed_div . Attending-Pending is also a Reference List column that pulls data from the EMPLOYEES table. $Attending_Pending.Role_Division.Division does the same as above except now we pull the division for each employee in the Attending-Pending column. We assign this list to the variable pending_div . We create a list of all divisions by adding the two lists together and assigning this combined list to the variable full_list . return sorted(set(full_list)) first uses Python\u2019s set() method to create a list with no duplicate divisions. We then use the sorted() method to return our set of divisions, sorted alphabetically. Note that the formula above can be simplified even further to: sorted( set($Attending_Confirmed.Role_Division.Division) | set($Attending_Pending.Role_Division.Division) )","title":"Example of Removing Duplicates from a List"},{"location":"formula-cheat-sheet/#setting-default-values-for-new-records","text":"You can set default values for when a new record is created and save yourself the trouble of having to fill in the same fields with the same values time after time. Read about it in the Community: Default values on the widget","title":"Setting Default Values for New Records"},{"location":"formula-cheat-sheet/#working-with-dates-and-times","text":"","title":"Working with dates and times"},{"location":"formula-cheat-sheet/#automatic-date-time-and-author-stamps","text":"You can automatically add the date or time a record was created or updated as well as who made the change.","title":"Automatic Date, Time and Author Stamps"},{"location":"formula-cheat-sheet/#examples-of-automatic-date-time-and-author-stamps","text":"Grant Application Tracker template The formula used in the Last Updated column of the Tasks table is: NOW() This is a trigger formula that triggers when a change is made to any field for this record. When a change is made, this formula runs its calculation. NOW() calculates the current time and date for the time zone selected. The formula used in the Created By column of the Tasks table is: user.Name This is a trigger formula that triggers when a new record is created. When the record is created, this formula runs its calculation. user.Name looks up the user account that is logged into Grist and returns the name associated with that account.","title":"Examples of Automatic Date, Time and Author Stamps"},{"location":"formula-cheat-sheet/#troubleshooting-errors_2","text":"If the time value in your datetime column is not calculating, check your formula. If TODAY() is used in DateTime, the time will always show 12:00am as you see below. NOW() is used for DateTime columns. TODAY() is used for Date. #AttributeError You have likely entered user.name but the formula is user.Name . Keep an eye on capitalization! #NameError You may have entered username or userName . The correct formula is user.Name . Another possibility is that this was entered in as a Formula column rather than a trigger formula column. Convert it to a trigger formula and this should resolve the problem.","title":"Troubleshooting Errors"},{"location":"formula-cheat-sheet/#filtering-data-within-a-specified-amount-of-time","text":"Using the DATEADD() function and comparision operators , you can determine if a date falls within a specific range then apply a filter.","title":"Filtering Data within a Specified Amount of Time"},{"location":"formula-cheat-sheet/#example-filtering-data-that-falls-in-1-month-range","text":"Community Example: Filtering Data Within a 1-Month Range The formula used in the Falls in 1 Month Range? column of the Interactions table is: TODAY() >= $Date >= DATEADD(TODAY(),months=-1) TODAY() returns the current date. $Date is the name of a column in our table, which is a Date type column. DATEADD(start_date, days=0, months=0, years=0, weeks=0) returns the date that is the given number of days, months, years, or weeks before or after the start_date . In this example, it returns the date that is one month prior to the start date, TODAY() . This formula is true if the date value in the Date column falls between TODAY() and our DATEADD() date which is one month ago. If the date value in the Date column does not fall between these two dates, the formula returns false. We can use this column to filter our data. If we only want to see interactions that fall within the 1 Month Range, we would filter to only include true values. If we want to see interactions that fall outside of the 1 Month Range, we would filter to only include false values.","title":"Example Filtering Data that 'Falls in 1 Month Range`"},{"location":"formula-cheat-sheet/#troubleshooting-errors_3","text":"#TypeError : Because $Date is a Date type column, TODAY() must be used in formulas comparing dates. NOW() is a DateTime formula that should only be used with other DateTime values. For example, if the $Date column was a DateTime type column, NOW() would need to be used rather than TODAY() because it includes the time component. NOW() is date and time. TODAY() is only date.","title":"Troubleshooting Errors"},{"location":"formula-timer/","text":"Formula timer # Grist has a built-in formula timer that will measure the time it takes to evaluate each formula in a document. This helps diagnose which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes. Grist\u2019s formula timer can be found on the \u2018Document Settings\u2019 page, under \u2018Data Engine\u2019. Select \u2018Start timing\u2019 to begin. On the next screen, you will have two choices, \u2018Start timing\u2019 and \u2018Time reload\u2019. Start timing \u2018Start timing\u2019 allows you to make changes to the document then stop timing to see the results. This is useful if you want to test specific formulas. You can make a change that affects that formula then come back and click \u2018Stop timing\u2019 to see the result. Time reload \u2018Time reload\u2019 forces a reload of the document while timing formulas and shows the result. This will show timing results for all formulas across the entire document. Results # Results are displayed in a table format. \u26a0\ufe0f Results Table The Formula Timer results table is not saved anywhere in the document. If you click away from this page, you will need to run the formula timer again to retrieve the table. Sort the Total Time column from Z > A so the formulas that take the longest time to run are listed first. The table specifies the Table ID and Column ID containing each formula. Review the formulas with the highest total time to see how they can be improved. If your document is experiencing slowness due to formula calculations, you\u2019ll see Total Times greater than 1 second. Need guidance on how to improve a formula? Post to our Community Forum !","title":"Formula timer"},{"location":"formula-timer/#formula-timer","text":"Grist has a built-in formula timer that will measure the time it takes to evaluate each formula in a document. This helps diagnose which formulas are responsible for slow performance when a document is first opened, or when a document responds to changes. Grist\u2019s formula timer can be found on the \u2018Document Settings\u2019 page, under \u2018Data Engine\u2019. Select \u2018Start timing\u2019 to begin. On the next screen, you will have two choices, \u2018Start timing\u2019 and \u2018Time reload\u2019. Start timing \u2018Start timing\u2019 allows you to make changes to the document then stop timing to see the results. This is useful if you want to test specific formulas. You can make a change that affects that formula then come back and click \u2018Stop timing\u2019 to see the result. Time reload \u2018Time reload\u2019 forces a reload of the document while timing formulas and shows the result. This will show timing results for all formulas across the entire document.","title":"Formula timer"},{"location":"formula-timer/#results","text":"Results are displayed in a table format. \u26a0\ufe0f Results Table The Formula Timer results table is not saved anywhere in the document. If you click away from this page, you will need to run the formula timer again to retrieve the table. Sort the Total Time column from Z > A so the formulas that take the longest time to run are listed first. The table specifies the Table ID and Column ID containing each formula. Review the formulas with the highest total time to see how they can be improved. If your document is experiencing slowness due to formula calculations, you\u2019ll see Total Times greater than 1 second. Need guidance on how to improve a formula? Post to our Community Forum !","title":"Results"},{"location":"formulas/","text":"Formulas # Grist has a powerful data engine to calculate the cells of your tables using formulas. It even has an AI Formula Assistant to help write formulas. If you\u2019ve used spreadsheets before, or database expressions, you\u2019ll be on familiar territory - but there are some wrinkles you\u2019ll want to know about, so hang around. Let\u2019s start with a classic use of spreadsheets. Suppose you have a list of products you\u2019ve ordered, the quantity you ordered, and the unit price of each. You\u2019ve made a column to show the quantity times the unit price, but want the computer to do that part for you. Just select a cell in the column you want to fill, and hit = key to tell Grist you want to enter a formula, rather than a value. Did you notice, when you did that, the labels of the columns changed a little? \u201cProduct\u201d became \u201c$Product\u201d, and \u201cUnit Price\u201d became \u201c$Unit_Price\u201d. This is Grist telling you how to refer to those columns in your formula. Just type $Quantity * $Unit_Price . You\u2019ll find an auto-complete feature ready to help you. Or if you don\u2019t like typing, click on the Quantity column, type the multiplication symbol, and then click on the Unit Price column. Your formula should look like this: To control the column ID, like \u201c$Unit_Price\u201d, that\u2019s used in formulas, see Renaming columns . Press Enter , and your formula is applied to all cells in the column. If you\u2019ve worked with spreadsheets before, you may be surprised that you don\u2019t need to specify row numbers, like B1 * C1 . In Grist, a single formula applies to a whole column. You don\u2019t have to worry about filling it in for all rows, and can refer to values in the same row without fuss. You can format numeric columns to look better by setting column type to Numeric , and selecting suitable formatting options: Column behavior # When we provide a formula for a column we tell Grist to update its value on every change in a document. We can no longer type a value into the cell, because its value is determined purely by the formula. A formula column is one of three possible column behaviors, which you can control using the COLUMN BEHAVIOR section in the creator panel: Data column maintains data, which you can manually update or clear, or optionally calculate using trigger formulas . Formula column always reflects the result of formula calculation, and is kept up-to-date by Grist. Empty column is a state for a new column. Typing any value into it will turn it into a Data Column, while typing in a formula will turn it into a Formula Column Using the COLUMN BEHAVIOR section, you can manually change the column behavior. The most common options are available as green action buttons at the bottom, and other options are available under the behavior menu. Depending on the current column behavior, those are: Set formula action converts an empty column to a formula column. Set trigger formula or Convert to trigger formula action sets a trigger on a column (more on triggers in the next Trigger formulas section ). Make into data column action converts an empty column to a regular data column. Convert column to data converts a formula column to regular data column (you can read more about this feature in the Freeze a formula column section ). Clear and make into formula clears all the data in a column and converts it to a formula column. (We say \u201cclear\u201d as a reminder that existing data in the column will be lost. They\u2019ll be replaced by the calculation results from the formula.) Clear and reset clears all data and completely resets the column to its initial Empty Column state. Python # Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Grist documents may use Python 2 or Python 3, see our Python guide for details. Formulas that operate over many rows # If you are a spreadsheet user, you may find yourself wanting to have some special rows at the end of your table that have formulas different to the rest. In Grist, we\u2019d like you to consider adding a widget to your page instead. For common use cases, Summary tables may be exactly what you need. Or if you want to set things up yourself, you can add an extra table widget like this (see Page widgets for details): This is just another table, giving us a place to put formulas outside of the structure of the Materials table. For example, if we wanted to count how many products there are in that table, we could use this formula: len(Materials.all) Every table in your document is available by its name in formulas, as a UserTable . This formula uses the all method to access the rows of the table, but doesn\u2019t do anything with them but count them. Here\u2019s a formula to compute the average price, using the Excel-like function AVERAGE : AVERAGE(Materials.all.Price) The all method returns a RecordSet , which supports iterating over individual columns this way. Equivalently, we could use a Python list comprehension : AVERAGE(material.Price for material in Materials.all) If you are not familiar with Python, it is worth following a tutorial. There are thousands online, including this official one . Python will be useful to you for all sorts of data work, not just Grist. List comprehension is useful once we\u2019re doing anything nuanced. For example, here\u2019s a formula to list the names of products with a quantity greater than 80: [m.Product for m in Materials.all if m.Quantity > 80] This is a list comprehension, but now with a conditional. The result is a list, which is rendered as text in a cell. Python can help in other ways in your search for rows. For example, here\u2019s a formula to find the name of the product with the highest quantity: max(Materials.all, key=lambda m: m.Quantity).Product Formulas are case-sensitive, with Excel-like functions being all-caps ( MAX ), and regular Python generally all lowercase ( max ). For exact matches, there is a shortcut to avoid iteration called lookupRecords , or lookupOne for single matches. Just pass the the values of columns you require to be matched. For example, here is a formula to look up the product name of a material with a quantity of 52: Materials.lookupOne(Quantity=52).Product For very large tables, it is wise to use lookups as much as you can, rather than iterating through rows. Returning to our example document, you can now see how we calculated the Total Spent , Average Quantity , and Most Ordered Product columns: Column Formula Total Spent SUM(Materials.all.Price) Average Quantity AVERAGE(Materials.all.Quantity) Most Ordered Product max(Materials.all, key=lambda m: m.Quantity).Product Separating out calculations like this from the body of your data can take some getting used to, but working this way can help keep your document more organized. And it brings other advantages. For example we could switch the formatting of the summary widget via the side panel: Varying formula by row # Having a formula apply to all rows is convenient and reduces the changes of mistakes. If you need to have a column change its behavior on different rows, it is possible using a conditional in the formula. For example, here is a replacement for the Materials.Price formula that ignores the price and shows zero for products whose name ends in \u201c(Sample)\u201d: if $Product.endswith(\"(Sample)\"): return 0 else: return $Quantity * $Unit_Price Code viewer # Once you have a lot of formulas, or if you have been invited to a document and want to get an overview of its formulas, there is a code viewer available with a pure Python summary of the document. Special values available in formulas # For those familiar with Python, here are the extra values available to you in Grist: rec is the current row. The $column syntax is shorthand for rec.column . The rec variable is of type Record . table is the current table, and is of type UserTable . Tables in your document are available by their name, and are also of type UserTable . Many extra spreadsheet functions are available, see the full function list . If your table or column has a space in its name, or other characters that are awkward in Python, those characters are replaced with an underscore. Auto-complete may help you if you\u2019re not sure. You can also control the \u201cids\u201d of columns and tables in the right side panel. Freeze a formula column # If you\u2019d like to save the output of your formula as plain values, you can simply change column behavior from Formula Column to Data Column . First open the column options in the side panel: Now click on the Formula Column and select Convert column to data option. Notice that there is no = sign in the column cells any more, showing that it is no longer a formula. The cells will no longer change if other cells they used to depend on change. The original formula is saved but stays inactive. It may come useful again if you wish to convert the column back to a formula column, or use it as a Trigger Formula . The side panel has lots of other handy settings, such as cell formatting (number of digits after decimal point, color, etc). The options apply just as much to formula columns as to regular columns. Lookups # Grist functions lookupOne and lookupRecords are useful for enumerating subsets of your data. For example, suppose we added a Category column to our Materials table, and wished to list all products belonging to a specific category. We can do this using TABLE.lookupRecords , where TABLE is the table of interest, and supplying it with the column values to match. For example, Materials.lookupRecords(Category='Ship') , as here: If you are following on, see Adding a field for details of how to add a new field to a card. If you care about the order of results, lookupRecords takes an optional sort_by parameter. For example, we could use this formula to sort by the product name itself: Materials.lookupRecords(Category='Ship', sort_by='Product').Product If you want to sort by multiple columns, remember that you can create a hidden formula column that combines data in any way you like, and then sort by that. The order of records returned by lookupRecords may not match the order of rows you see in a table. To get that order, use sort_by='manualSort' . This is an internal column that is updated with the manually established sort order of rows. If you find yourself doing a lot of look-ups, please consider whether Summary tables and Summary formulas might be what you are looking for. Recursion # Lookups are handy for recursive formulas. Suppose we have a table counting how many events we have per day, and want to add a cumulative sum of those event counts. One way to do that is with a formula like this: yesterday = Events.lookupOne(date=$date - datetime.timedelta(days=1)) $events + (yesterday.cumulative or 0) For clarity, we\u2019ve split this formula into two lines. The first line makes a variable pointing to the row of the day before. The second line computes the value we want in the cell. Python note: the value of the last line is automatically returned (you could prefix it with return if you like). Notice the yesterday.cumulative or 0 . For the earliest row in the table, there will be no yesterday. In this case, lookupOne returns a special empty record, for which yesterday.cumulative will be None . If you\u2019d like to simplify this formula, or find yourself using the same lookup in multiple formulas, it would be worth making yesterday a reference column . Simply add a reference column, and give a formula for it that matches how we defined yesterday here. To actually enter this formula in a cell, you\u2019d use Shift + Enter to divide the lines. Trigger Formulas # Formula columns are great for calculated values \u2013 those determined by other data in the document. It may also be useful to store independent data in a column, but still use a formula to calculate it in some situations. This is exactly what Trigger Formulas offer. It is a very powerful feature that allows you to create a Timestamp or Authorship column, recalculate your data based on a set of conditions that you decide , clean data when a new value is entered, or provide sensible default value for a column. To create a Trigger Formula column, you first need to open the creator panel and click on the Set trigger formula action. If you want to convert an existing formula, use the Convert to trigger formula action available in the COLUMN BEHAVIOR section. To control when the formula is evaluated, use the two checkbox options below: Apply to new records triggers the formula only when a new record is created (a default cell value). Apply on record changes triggers the formula when a record is updated. Applying to new records is self-explanatory, the formula will be evaluated only once when you add a new record. It is a perfect solution to provide default values to the empty cells. Second option allows you to fine grain the conditions and specify which columns, when updated, will trigger the evaluation: You probably noticed the first option Current field . At first glance, you probably wonder: \u201cWhy would I want to trigger the column on its own change?\u201d. This option allows you to react to a value that is being entered into the column, just before it is saved! In the formula editor, you have access to two variables that are not available to regular formulas: value which is the value that a user wants to enter, user which represents a user object that is making the change (you will also see this in the Access rules section). This allows you to make your application even smarter, track when a record was updated , or see who made the last change to a row . Simple examples: Ensure that the value in a column is always written in capital letters: With the trigger formula of value.upper() , the value typed into this column will be converted to upper case automatically. Format a value that the user enters to sanitize the data before saving: With the formula like value if value.startswith(\"SK\") else \"SK\" + value , the value typed into this column will always be prefixed with \u201cSK\u201d. Overwrite a default value from a referenced table: You can use a formula like value or $Client.Phone , to provide a default value from a referenced table, but still allow the user to type a new one. In each of these examples, when the user tries to modify a cell, Grist (before updating the record) will evaluate the formula and store its result in the column instead of the value provided by the user. For a detailed, real-life example read our guide on how to create time and user stamps. For more information on formulas and trigger formulas, check out our webinar Trigger Formulas v. Formulas .","title":"Intro to formulas"},{"location":"formulas/#formulas","text":"Grist has a powerful data engine to calculate the cells of your tables using formulas. It even has an AI Formula Assistant to help write formulas. If you\u2019ve used spreadsheets before, or database expressions, you\u2019ll be on familiar territory - but there are some wrinkles you\u2019ll want to know about, so hang around. Let\u2019s start with a classic use of spreadsheets. Suppose you have a list of products you\u2019ve ordered, the quantity you ordered, and the unit price of each. You\u2019ve made a column to show the quantity times the unit price, but want the computer to do that part for you. Just select a cell in the column you want to fill, and hit = key to tell Grist you want to enter a formula, rather than a value. Did you notice, when you did that, the labels of the columns changed a little? \u201cProduct\u201d became \u201c$Product\u201d, and \u201cUnit Price\u201d became \u201c$Unit_Price\u201d. This is Grist telling you how to refer to those columns in your formula. Just type $Quantity * $Unit_Price . You\u2019ll find an auto-complete feature ready to help you. Or if you don\u2019t like typing, click on the Quantity column, type the multiplication symbol, and then click on the Unit Price column. Your formula should look like this: To control the column ID, like \u201c$Unit_Price\u201d, that\u2019s used in formulas, see Renaming columns . Press Enter , and your formula is applied to all cells in the column. If you\u2019ve worked with spreadsheets before, you may be surprised that you don\u2019t need to specify row numbers, like B1 * C1 . In Grist, a single formula applies to a whole column. You don\u2019t have to worry about filling it in for all rows, and can refer to values in the same row without fuss. You can format numeric columns to look better by setting column type to Numeric , and selecting suitable formatting options:","title":"Formulas"},{"location":"formulas/#column-behavior","text":"When we provide a formula for a column we tell Grist to update its value on every change in a document. We can no longer type a value into the cell, because its value is determined purely by the formula. A formula column is one of three possible column behaviors, which you can control using the COLUMN BEHAVIOR section in the creator panel: Data column maintains data, which you can manually update or clear, or optionally calculate using trigger formulas . Formula column always reflects the result of formula calculation, and is kept up-to-date by Grist. Empty column is a state for a new column. Typing any value into it will turn it into a Data Column, while typing in a formula will turn it into a Formula Column Using the COLUMN BEHAVIOR section, you can manually change the column behavior. The most common options are available as green action buttons at the bottom, and other options are available under the behavior menu. Depending on the current column behavior, those are: Set formula action converts an empty column to a formula column. Set trigger formula or Convert to trigger formula action sets a trigger on a column (more on triggers in the next Trigger formulas section ). Make into data column action converts an empty column to a regular data column. Convert column to data converts a formula column to regular data column (you can read more about this feature in the Freeze a formula column section ). Clear and make into formula clears all the data in a column and converts it to a formula column. (We say \u201cclear\u201d as a reminder that existing data in the column will be lost. They\u2019ll be replaced by the calculation results from the formula.) Clear and reset clears all data and completely resets the column to its initial Empty Column state.","title":"Column behavior"},{"location":"formulas/#python","text":"Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Grist documents may use Python 2 or Python 3, see our Python guide for details.","title":"Python"},{"location":"formulas/#formulas-that-operate-over-many-rows","text":"If you are a spreadsheet user, you may find yourself wanting to have some special rows at the end of your table that have formulas different to the rest. In Grist, we\u2019d like you to consider adding a widget to your page instead. For common use cases, Summary tables may be exactly what you need. Or if you want to set things up yourself, you can add an extra table widget like this (see Page widgets for details): This is just another table, giving us a place to put formulas outside of the structure of the Materials table. For example, if we wanted to count how many products there are in that table, we could use this formula: len(Materials.all) Every table in your document is available by its name in formulas, as a UserTable . This formula uses the all method to access the rows of the table, but doesn\u2019t do anything with them but count them. Here\u2019s a formula to compute the average price, using the Excel-like function AVERAGE : AVERAGE(Materials.all.Price) The all method returns a RecordSet , which supports iterating over individual columns this way. Equivalently, we could use a Python list comprehension : AVERAGE(material.Price for material in Materials.all) If you are not familiar with Python, it is worth following a tutorial. There are thousands online, including this official one . Python will be useful to you for all sorts of data work, not just Grist. List comprehension is useful once we\u2019re doing anything nuanced. For example, here\u2019s a formula to list the names of products with a quantity greater than 80: [m.Product for m in Materials.all if m.Quantity > 80] This is a list comprehension, but now with a conditional. The result is a list, which is rendered as text in a cell. Python can help in other ways in your search for rows. For example, here\u2019s a formula to find the name of the product with the highest quantity: max(Materials.all, key=lambda m: m.Quantity).Product Formulas are case-sensitive, with Excel-like functions being all-caps ( MAX ), and regular Python generally all lowercase ( max ). For exact matches, there is a shortcut to avoid iteration called lookupRecords , or lookupOne for single matches. Just pass the the values of columns you require to be matched. For example, here is a formula to look up the product name of a material with a quantity of 52: Materials.lookupOne(Quantity=52).Product For very large tables, it is wise to use lookups as much as you can, rather than iterating through rows. Returning to our example document, you can now see how we calculated the Total Spent , Average Quantity , and Most Ordered Product columns: Column Formula Total Spent SUM(Materials.all.Price) Average Quantity AVERAGE(Materials.all.Quantity) Most Ordered Product max(Materials.all, key=lambda m: m.Quantity).Product Separating out calculations like this from the body of your data can take some getting used to, but working this way can help keep your document more organized. And it brings other advantages. For example we could switch the formatting of the summary widget via the side panel:","title":"Formulas that operate over many rows"},{"location":"formulas/#varying-formula-by-row","text":"Having a formula apply to all rows is convenient and reduces the changes of mistakes. If you need to have a column change its behavior on different rows, it is possible using a conditional in the formula. For example, here is a replacement for the Materials.Price formula that ignores the price and shows zero for products whose name ends in \u201c(Sample)\u201d: if $Product.endswith(\"(Sample)\"): return 0 else: return $Quantity * $Unit_Price","title":"Varying formula by row"},{"location":"formulas/#code-viewer","text":"Once you have a lot of formulas, or if you have been invited to a document and want to get an overview of its formulas, there is a code viewer available with a pure Python summary of the document.","title":"Code viewer"},{"location":"formulas/#special-values-available-in-formulas","text":"For those familiar with Python, here are the extra values available to you in Grist: rec is the current row. The $column syntax is shorthand for rec.column . The rec variable is of type Record . table is the current table, and is of type UserTable . Tables in your document are available by their name, and are also of type UserTable . Many extra spreadsheet functions are available, see the full function list . If your table or column has a space in its name, or other characters that are awkward in Python, those characters are replaced with an underscore. Auto-complete may help you if you\u2019re not sure. You can also control the \u201cids\u201d of columns and tables in the right side panel.","title":"Special values available in formulas"},{"location":"formulas/#freeze-a-formula-column","text":"If you\u2019d like to save the output of your formula as plain values, you can simply change column behavior from Formula Column to Data Column . First open the column options in the side panel: Now click on the Formula Column and select Convert column to data option. Notice that there is no = sign in the column cells any more, showing that it is no longer a formula. The cells will no longer change if other cells they used to depend on change. The original formula is saved but stays inactive. It may come useful again if you wish to convert the column back to a formula column, or use it as a Trigger Formula . The side panel has lots of other handy settings, such as cell formatting (number of digits after decimal point, color, etc). The options apply just as much to formula columns as to regular columns.","title":"Freeze a formula column"},{"location":"formulas/#lookups","text":"Grist functions lookupOne and lookupRecords are useful for enumerating subsets of your data. For example, suppose we added a Category column to our Materials table, and wished to list all products belonging to a specific category. We can do this using TABLE.lookupRecords , where TABLE is the table of interest, and supplying it with the column values to match. For example, Materials.lookupRecords(Category='Ship') , as here: If you are following on, see Adding a field for details of how to add a new field to a card. If you care about the order of results, lookupRecords takes an optional sort_by parameter. For example, we could use this formula to sort by the product name itself: Materials.lookupRecords(Category='Ship', sort_by='Product').Product If you want to sort by multiple columns, remember that you can create a hidden formula column that combines data in any way you like, and then sort by that. The order of records returned by lookupRecords may not match the order of rows you see in a table. To get that order, use sort_by='manualSort' . This is an internal column that is updated with the manually established sort order of rows. If you find yourself doing a lot of look-ups, please consider whether Summary tables and Summary formulas might be what you are looking for.","title":"Lookups"},{"location":"formulas/#recursion","text":"Lookups are handy for recursive formulas. Suppose we have a table counting how many events we have per day, and want to add a cumulative sum of those event counts. One way to do that is with a formula like this: yesterday = Events.lookupOne(date=$date - datetime.timedelta(days=1)) $events + (yesterday.cumulative or 0) For clarity, we\u2019ve split this formula into two lines. The first line makes a variable pointing to the row of the day before. The second line computes the value we want in the cell. Python note: the value of the last line is automatically returned (you could prefix it with return if you like). Notice the yesterday.cumulative or 0 . For the earliest row in the table, there will be no yesterday. In this case, lookupOne returns a special empty record, for which yesterday.cumulative will be None . If you\u2019d like to simplify this formula, or find yourself using the same lookup in multiple formulas, it would be worth making yesterday a reference column . Simply add a reference column, and give a formula for it that matches how we defined yesterday here. To actually enter this formula in a cell, you\u2019d use Shift + Enter to divide the lines.","title":"Recursion"},{"location":"formulas/#trigger-formulas","text":"Formula columns are great for calculated values \u2013 those determined by other data in the document. It may also be useful to store independent data in a column, but still use a formula to calculate it in some situations. This is exactly what Trigger Formulas offer. It is a very powerful feature that allows you to create a Timestamp or Authorship column, recalculate your data based on a set of conditions that you decide , clean data when a new value is entered, or provide sensible default value for a column. To create a Trigger Formula column, you first need to open the creator panel and click on the Set trigger formula action. If you want to convert an existing formula, use the Convert to trigger formula action available in the COLUMN BEHAVIOR section. To control when the formula is evaluated, use the two checkbox options below: Apply to new records triggers the formula only when a new record is created (a default cell value). Apply on record changes triggers the formula when a record is updated. Applying to new records is self-explanatory, the formula will be evaluated only once when you add a new record. It is a perfect solution to provide default values to the empty cells. Second option allows you to fine grain the conditions and specify which columns, when updated, will trigger the evaluation: You probably noticed the first option Current field . At first glance, you probably wonder: \u201cWhy would I want to trigger the column on its own change?\u201d. This option allows you to react to a value that is being entered into the column, just before it is saved! In the formula editor, you have access to two variables that are not available to regular formulas: value which is the value that a user wants to enter, user which represents a user object that is making the change (you will also see this in the Access rules section). This allows you to make your application even smarter, track when a record was updated , or see who made the last change to a row . Simple examples: Ensure that the value in a column is always written in capital letters: With the trigger formula of value.upper() , the value typed into this column will be converted to upper case automatically. Format a value that the user enters to sanitize the data before saving: With the formula like value if value.startswith(\"SK\") else \"SK\" + value , the value typed into this column will always be prefixed with \u201cSK\u201d. Overwrite a default value from a referenced table: You can use a formula like value or $Client.Phone , to provide a default value from a referenced table, but still allow the user to type a new one. In each of these examples, when the user tries to modify a cell, Grist (before updating the record) will evaluate the formula and store its result in the column instead of the value provided by the user. For a detailed, real-life example read our guide on how to create time and user stamps. For more information on formulas and trigger formulas, check out our webinar Trigger Formulas v. Formulas .","title":"Trigger Formulas"},{"location":"functions/","text":"Function Reference # Grist formulas support most Excel functions, as well as the Python programming language. The table below lists Grist-specific functions, and the suite of the included Excel-like functions. In addition, the entire Python standard library is available. For more about using formulas in Grist, see Intro to Formulas . Grist uses Python (version 3.11) for formulas. You can use nearly all features of Python (see Python documentation ). Here are some helpful notes: Python is case-sensitive, including for Grist table and column names. Excel-like functions are always in uppercase. E.g. if is a Python keyword, while IF is an Excel-like function. Compare for equality using == , in place of Excel\u2019s single = (which in Python means assignment). \u201cNot equal\u201d uses != in place of Excel\u2019s <> . You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. Category Functions Grist Record or rec , $Field or rec.Field , $group or rec.group , RecordSet , UserTable , all , lookupOne , lookupRecords Date DATE , DATEADD , DATEDIF , DATEVALUE , DATE_TO_XL , DAY , DAYS , DTIME , EDATE , EOMONTH , HOUR , ISOWEEKNUM , MINUTE , MONTH , NOW , SECOND , TODAY , WEEKDAY , WEEKNUM , XL_TO_DATE , YEAR , YEARFRAC Info CELL , CURRENT_CONVERSION , ISBLANK , ISEMAIL , ISERR , ISERROR , ISLOGICAL , ISNA , ISNONTEXT , ISNUMBER , ISREF , ISREFLIST , ISTEXT , ISURL , N , NA , PEEK , RECORD , REQUEST , TYPE Logical AND , FALSE , IF , IFERROR , NOT , OR , TRUE Lookup lookupOne , lookupRecords , ADDRESS , CHOOSE , COLUMN , COLUMNS , CONTAINS , GETPIVOTDATA , HLOOKUP , HYPERLINK , INDEX , INDIRECT , LOOKUP , MATCH , OFFSET , ROW , ROWS , SELF_HYPERLINK , VLOOKUP Math ABS , ACOS , ACOSH , ARABIC , ASIN , ASINH , ATAN , ATAN2 , ATANH , CEILING , COMBIN , COS , COSH , DEGREES , EVEN , EXP , FACT , FACTDOUBLE , FLOOR , GCD , INT , LCM , LN , LOG , LOG10 , MOD , MROUND , MULTINOMIAL , ODD , PI , POWER , PRODUCT , QUOTIENT , RADIANS , RAND , RANDBETWEEN , ROMAN , ROUND , ROUNDDOWN , ROUNDUP , SERIESSUM , SIGN , SIN , SINH , SQRT , SQRTPI , SUBTOTAL , SUM , SUMIF , SUMIFS , SUMPRODUCT , SUMSQ , TAN , TANH , TRUNC , UUID Schedule SCHEDULE Stats AVEDEV , AVERAGE , AVERAGEA , AVERAGEIF , AVERAGEIFS , AVERAGE_WEIGHTED , BINOMDIST , CONFIDENCE , CORREL , COUNT , COUNTA , COVAR , CRITBINOM , DEVSQ , EXPONDIST , FDIST , FISHER , FISHERINV , FORECAST , F_DIST , F_DIST_RT , GEOMEAN , HARMEAN , HYPGEOMDIST , INTERCEPT , KURT , LARGE , LOGINV , LOGNORMDIST , MAX , MAXA , MEDIAN , MIN , MINA , MODE , NEGBINOMDIST , NORMDIST , NORMINV , NORMSDIST , NORMSINV , PEARSON , PERCENTILE , PERCENTRANK , PERCENTRANK_EXC , PERCENTRANK_INC , PERMUT , POISSON , PROB , QUARTILE , RANK , RANK_AVG , RANK_EQ , RSQ , SKEW , SLOPE , SMALL , STANDARDIZE , STDEV , STDEVA , STDEVP , STDEVPA , STEYX , TDIST , TINV , TRIMMEAN , TTEST , T_INV , T_INV_2T , VAR , VARA , VARP , VARPA , WEIBULL , ZTEST Text CHAR , CLEAN , CODE , CONCAT , CONCATENATE , DOLLAR , EXACT , FIND , FIXED , LEFT , LEN , LOWER , MID , PHONE_FORMAT , PROPER , REGEXEXTRACT , REGEXMATCH , REGEXREPLACE , REPLACE , REPT , RIGHT , SEARCH , SUBSTITUTE , T , TEXT , TRIM , UPPER , VALUE Grist # Record # class Record # A Record represents a record of data. It is the primary means of accessing values in formulas. A Record for a particular table has a property for each data and formula column in the table. In a formula, $field is translated to rec.field , where rec is the Record for which the formula is being evaluated. For example: def Full_Name(rec, table): return rec.First_Name + ' ' + rec.LastName def Name_Length(rec, table): return len(rec.Full_Name) $Field # $ Field or rec .Field # Access the field named \u201cField\u201d of the current record. E.g. $First_Name or rec.First_Name . $group # $group # In a summary table , $group is a special field containing the list of Records that are summarized by the current summary line. E.g. the formula len($group) counts the number of those records being summarized in each row. See RecordSet for useful properties offered by the returned object. Examples: sum($group.Amount) # Sum of the Amount field in the matching records sum(r.Amount for r in $group) # Same as sum($group.Amount) sum(r.Amount for r in $group if r > 0) # Sum of only the positive amounts sum(r.Shares * r.Price for r in $group) # Sum of shares * price products RecordSet # class RecordSet # A RecordSet represents a collection of records, as returned by Table.lookupRecords() or $group property in summary views. A RecordSet allows iterating through the records: sum(r.Amount for r in Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\")) min(r.DueDate for r in Tasks.lookupRecords(Owner=\"Bob\")) RecordSets also provide a convenient way to access the list of values for a particular field for all the records, as record_set.Field . For example, the examples above are equivalent to: sum(Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\").Amount) min(Tasks.lookupRecords(Owner=\"Bob\").DueDate) You can get the number of records in a RecordSet using len , e.g. len($group) . UserTable # class UserTable # Each data table in the document is represented in the code by an instance of UserTable class. These names are always capitalized. A UserTable provides access to all the records in the table, as well as methods to look up particular records. Every table in the document is available to all formulas. all # UserTable. all # The list of all the records in this table. For example, this evaluates to the number of records in the table Students . len(Students.all) This evaluates to the sum of the Population field for every record in the table Countries . sum(r.Population for r in Countries.all) lookupOne # UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. If sort_by=field is given, sort the results by that field. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email) People.lookupOne(Email=$Work_Email, sort_by=\"Date\") lookupRecords # UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. Date # DATE # DATE (year, month, day) # Returns the datetime.datetime object that represents a particular date. The DATE function is most useful in formulas where year, month, and day are formulas, not constants. If year is between 0 and 1899 (inclusive), adds 1900 to calculate the year. >>> DATE(108, 1, 2) datetime.date(2008, 1, 2) >>> DATE(2008, 1, 2) datetime.date(2008, 1, 2) If month is greater than 12, rolls into the following year. >>> DATE(2008, 14, 2) datetime.date(2009, 2, 2) If month is less than 1, subtracts that many months plus 1, from the first month in the year. >>> DATE(2008, -3, 2) datetime.date(2007, 9, 2) If day is greater than the number of days in the given month, rolls into the following months. >>> DATE(2008, 1, 35) datetime.date(2008, 2, 4) If day is less than 1, subtracts that many days plus 1, from the first day of the given month. >>> DATE(2008, 1, -15) datetime.date(2007, 12, 16) DATEADD # DATEADD (start_date, days=0, months=0, years=0, weeks=0) # Returns the date a given number of days, months, years, or weeks away from start_date . You may specify arguments in any order if you specify argument names. Use negative values to subtract. For example, DATEADD(date, 1) is the same as DATEADD(date, days=1) , ands adds one day to date . DATEADD(date, years=1, days=-1) adds one year minus one day. >>> DATEADD(DATE(2011, 1, 15), 1) datetime.date(2011, 1, 16) >>> DATEADD(DATE(2011, 1, 15), months=1, days=-1) datetime.date(2011, 2, 14) >>> DATEADD(DATE(2011, 1, 15), years=-2, months=1, days=3, weeks=2) datetime.date(2009, 3, 4) >>> DATEADD(DATE(1975, 4, 30), years=50, weeks=-5) datetime.date(2025, 3, 26) DATEDIF # DATEDIF (start_date, end_date, unit) # Calculates the number of days, months, or years between two dates. Unit indicates the type of information that you want returned: \u201cY\u201d: The number of complete years in the period. \u201cM\u201d: The number of complete months in the period. \u201cD\u201d: The number of days in the period. \u201cMD\u201d: The difference between the days in start_date and end_date. The months and years of the dates are ignored. \u201cYM\u201d: The difference between the months in start_date and end_date. The days and years of the dates are ignored. \u201cYD\u201d: The difference between the days of start_date and end_date. The years of the dates are ignored. Two complete years in the period (2) >>> DATEDIF(DATE(2001, 1, 1), DATE(2003, 1, 1), \"Y\") 2 440 days between June 1, 2001, and August 15, 2002 (440) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"D\") 440 75 days between June 1 and August 15, ignoring the years of the dates (75) >>> DATEDIF(DATE(2001, 6, 1), DATE(2012, 8, 15), \"YD\") 75 The difference between 1 and 15, ignoring the months and the years of the dates (14) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"MD\") 14 DATEVALUE # DATEVALUE (date_string, tz=None) # Converts a date that is stored as text to a datetime object. >>> DATEVALUE(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"30-Jan-2008\") datetime.datetime(2008, 1, 30, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"2008-12-11\") datetime.datetime(2008, 12, 11, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"5-JUL\").replace(year=2000) datetime.datetime(2000, 7, 5, 0, 0, tzinfo=moment.tzinfo('America/New_York')) In case of ambiguity, prefer M/D/Y format. >>> DATEVALUE(\"1/2/3\") datetime.datetime(2003, 1, 2, 0, 0, tzinfo=moment.tzinfo('America/New_York')) DATE_TO_XL # DATE_TO_XL (date_value) # Converts a Python date or datetime object to the serial number as used by Excel, with December 30, 1899 as serial number 1. See XL_TO_DATE for more explanation. >>> DATE_TO_XL(datetime.date(2008, 1, 1)) 39448.0 >>> DATE_TO_XL(datetime.date(2012, 3, 14)) 40982.0 >>> DATE_TO_XL(datetime.datetime(2012, 3, 14, 1, 30)) 40982.0625 DAY # DAY (date) # Returns the day of a date, as an integer ranging from 1 to 31. Same as date.day . >>> DAY(DATE(2011, 4, 15)) 15 >>> DAY(\"5/31/2012\") 31 >>> DAY(datetime.datetime(1900, 1, 1)) 1 DAYS # DAYS (end_date, start_date) # Returns the number of days between two dates. Same as (end_date - start_date).days . >>> DAYS(\"3/15/11\",\"2/1/11\") 42 >>> DAYS(DATE(2011, 12, 31), DATE(2011, 1, 1)) 364 >>> DAYS(\"2/1/11\", \"3/15/11\") -42 DTIME # DTIME (value, tz=None) # Returns the value converted to a python datetime object. The value may be a string , date (interpreted as midnight on that day), time (interpreted as a time-of-day today), or an existing datetime . The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. If the input is itself a datetime with the timezone set, it is returned unchanged (no changes to its timezone). >>> DTIME(datetime.date(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.date(2017, 1, 1), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('Europe/Paris')) >>> DTIME(datetime.datetime(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC'))) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC')), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) EDATE # EDATE (start_date, months) # Returns the date that is the given number of months before or after start_date . Use EDATE to calculate maturity dates or due dates that fall on the same day of the month as the date of issue. >>> EDATE(DATE(2011, 1, 15), 1) datetime.date(2011, 2, 15) >>> EDATE(DATE(2011, 1, 15), -1) datetime.date(2010, 12, 15) >>> EDATE(DATE(2011, 1, 15), 2) datetime.date(2011, 3, 15) >>> EDATE(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 1) >>> EDATE(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 1) EOMONTH # EOMONTH (start_date, months) # Returns the date for the last day of the month that is the indicated number of months before or after start_date. Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. >>> EOMONTH(DATE(2011, 1, 1), 1) datetime.date(2011, 2, 28) >>> EOMONTH(DATE(2011, 1, 15), -3) datetime.date(2010, 10, 31) >>> EOMONTH(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 31) >>> EOMONTH(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 31) HOUR # HOUR (time) # Same as time.hour . >>> HOUR(XL_TO_DATE(0.75)) 18 >>> HOUR(\"7/18/2011 7:45\") 7 >>> HOUR(\"4/21/2012\") 0 ISOWEEKNUM # ISOWEEKNUM (date) # Returns the ISO week number of the year for a given date. >>> ISOWEEKNUM(\"3/9/2012\") 10 >>> [ISOWEEKNUM(DATE(2000 + y, 1, 1)) for y in [0,1,2,3,4,5,6,7,8]] [52, 1, 1, 1, 1, 53, 52, 1, 1] MINUTE # MINUTE (time) # Returns the minutes of datetime , as an integer from 0 to 59. Same as time.minute . >>> MINUTE(XL_TO_DATE(0.75)) 0 >>> MINUTE(\"7/18/2011 7:45\") 45 >>> MINUTE(\"12:59:00 PM\") 59 >>> MINUTE(datetime.time(12, 58, 59)) 58 MONTH # MONTH (date) # Returns the month of a date represented, as an integer from from 1 (January) to 12 (December). Same as date.month . >>> MONTH(DATE(2011, 4, 15)) 4 >>> MONTH(\"5/31/2012\") 5 >>> MONTH(datetime.datetime(1900, 1, 1)) 1 NOW # NOW (tz=None) # Returns the datetime object for the current time. SECOND # SECOND (time) # Returns the seconds of datetime , as an integer from 0 to 59. Same as time.second . >>> SECOND(XL_TO_DATE(0.75)) 0 >>> SECOND(\"7/18/2011 7:45:13\") 13 >>> SECOND(datetime.time(12, 58, 59)) 59 TODAY # TODAY (tz=None) # Returns the date object for the current date. WEEKDAY # WEEKDAY (date, return_type=1) # Returns the day of the week corresponding to a date. The day is given as an integer, ranging from 1 (Sunday) to 7 (Saturday), by default. Return_type determines the type of the returned value. 1 (default) - Returns 1 (Sunday) through 7 (Saturday). 2 - Returns 1 (Monday) through 7 (Sunday). 3 - Returns 0 (Monday) through 6 (Sunday). 11 - Returns 1 (Monday) through 7 (Sunday). 12 - Returns 1 (Tuesday) through 7 (Monday). 13 - Returns 1 (Wednesday) through 7 (Tuesday). 14 - Returns 1 (Thursday) through 7 (Wednesday). 15 - Returns 1 (Friday) through 7 (Thursday). 16 - Returns 1 (Saturday) through 7 (Friday). 17 - Returns 1 (Sunday) through 7 (Saturday). >>> WEEKDAY(DATE(2008, 2, 14)) 5 >>> WEEKDAY(DATE(2012, 3, 1)) 5 >>> WEEKDAY(DATE(2012, 3, 1), 1) 5 >>> WEEKDAY(DATE(2012, 3, 1), 2) 4 >>> WEEKDAY(\"3/1/2012\", 3) 3 WEEKNUM # WEEKNUM (date, return_type=1) # Returns the week number of a specific date. For example, the week containing January 1 is the first week of the year, and is numbered week 1. Return_type determines which week is considered the first week of the year. 1 (default) - Week 1 is the first week starting Sunday that contains January 1. 2 - Week 1 is the first week starting Monday that contains January 1. 11 - Week 1 is the first week starting Monday that contains January 1. 12 - Week 1 is the first week starting Tuesday that contains January 1. 13 - Week 1 is the first week starting Wednesday that contains January 1. 14 - Week 1 is the first week starting Thursday that contains January 1. 15 - Week 1 is the first week starting Friday that contains January 1. 16 - Week 1 is the first week starting Saturday that contains January 1. 17 - Week 1 is the first week starting Sunday that contains January 1. 21 - ISO 8601 Approach: Week 1 is the first week starting Monday that contains January 4. Equivalently, it is the week that contains the first Thursday of the year. >>> WEEKNUM(DATE(2012, 3, 9)) 10 >>> WEEKNUM(DATE(2012, 3, 9), 2) 11 >>> WEEKNUM('1/1/1900') 1 >>> WEEKNUM('2/1/1900') 5 XL_TO_DATE # XL_TO_DATE (value, tz=None) # Converts a provided Excel serial number representing a date into a datetime object. Value is interpreted as the number of days since December 30, 1899. (This corresponds to Google Sheets interpretation. Excel starts with Dec. 31, 1899 but wrongly considers 1900 to be a leap year. Excel for Mac should be configured to use 1900 date system, i.e. uncheck \u201cUse the 1904 date system\u201d option.) The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. >>> XL_TO_DATE(41100.1875) datetime.datetime(2012, 7, 10, 4, 30, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(39448) datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(40982.0625) datetime.datetime(2012, 3, 14, 1, 30, tzinfo=moment.tzinfo('America/New_York')) YEAR # YEAR (date) # Returns the year corresponding to a date as an integer. Same as date.year . >>> YEAR(DATE(2011, 4, 15)) 2011 >>> YEAR(\"5/31/2030\") 2030 >>> YEAR(datetime.datetime(1900, 1, 1)) 1900 YEARFRAC # YEARFRAC (start_date, end_date, basis=0) # Calculates the fraction of the year represented by the number of whole days between two dates. Basis is the type of day count basis to use. 0 (default) - US (NASD) 30/360 1 - Actual/actual 2 - Actual/360 3 - Actual/365 4 - European 30/360 -1 - Actual/actual (Google Sheets variation) This function is useful for financial calculations. For compatibility with Excel, it defaults to using the NASD standard calendar. For use in non-financial settings, option -1 is likely the best choice. See https://en.wikipedia.org/wiki/360-day_calendar for explanation of the US 30/360 and European 30/360 methods. See http://www.dwheeler.com/yearfrac/ for analysis of Excel\u2019s particular implementation. Basis -1 is similar to 1 , but differs from Excel when dates span both leap and non-leap years. It matches the calculation in Google Sheets, counting the days in each year as a fraction of that year\u2019s length. Fraction of the year between 1/1/2012 and 7/30/12, omitting the Basis argument. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30)) '0.58055556' Fraction between same dates, using the Actual/Actual basis argument. Because 2012 is a Leap year, it has a 366 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 1) '0.57650273' Fraction between same dates, using the Actual/365 basis argument. Uses a 365 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 3) '0.57808219' Info # CELL # CELL (info_type, reference) # Returns the requested information about the specified cell. This is not implemented in Grist Note This function is not currently implemented in Grist. CURRENT_CONVERSION # CURRENT_CONVERSION (rec) # Internal function used by Grist during column type conversions. Not available for use in formulas. ISBLANK # ISBLANK (value) # Returns whether a value refers to an empty cell. It isn\u2019t implemented in Grist. To check for an empty string, use value == \"\" . Note This function is not currently implemented in Grist. ISEMAIL # ISEMAIL (value) # Returns whether a value is a valid email address. Note that checking email validity is not an exact science. The technical standard considers many email addresses valid that are not used in practice, and would not be considered valid by most users. Instead, we follow Google Sheets implementation, with some differences, noted below. >>> ISEMAIL(\"Abc.123@example.com\") True >>> ISEMAIL(\"Bob_O-Reilly+tag@example.com\") True >>> ISEMAIL(\"John Doe\") False >>> ISEMAIL(\"john@aol...com\") False ISERR # ISERR (value) # Checks whether a value is an error. In other words, it returns true if using value directly would raise an exception. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. A more Pythonic approach to checking for errors is: try: ... value ... except Exception, err: ... do something about the error ... For example: >>> ISERR(\"Hello\") False ISERROR # ISERROR (value) # Checks whether a value is an error or an invalid value. It is similar to ISERR , but also returns true for an invalid value such as NaN or a text value in a Numeric column. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. >>> ISERROR(\"Hello\") False >>> ISERROR(AltText(\"fail\")) True >>> ISERROR(float('nan')) True ISLOGICAL # ISLOGICAL (value) # Checks whether a value is True or False . >>> ISLOGICAL(True) True >>> ISLOGICAL(False) True >>> ISLOGICAL(0) False >>> ISLOGICAL(None) False >>> ISLOGICAL(\"Test\") False ISNA # ISNA (value) # Checks whether a value is the error #N/A . >>> ISNA(float('nan')) True >>> ISNA(0.0) False >>> ISNA('text') False >>> ISNA(float('-inf')) False ISNONTEXT # ISNONTEXT (value) # Checks whether a value is non-textual. >>> ISNONTEXT(\"asdf\") False >>> ISNONTEXT(\"\") False >>> ISNONTEXT(AltText(\"text\")) False >>> ISNONTEXT(17.0) True >>> ISNONTEXT(None) True >>> ISNONTEXT(datetime.date(2011, 1, 1)) True ISNUMBER # ISNUMBER (value) # Checks whether a value is a number. >>> ISNUMBER(17) True >>> ISNUMBER(-123.123423) True >>> ISNUMBER(False) True >>> ISNUMBER(float('nan')) True >>> ISNUMBER(float('inf')) True >>> ISNUMBER('17') False >>> ISNUMBER(None) False >>> ISNUMBER(datetime.date(2011, 1, 1)) False ISREF # ISREF (value) # Checks whether a value is a table record. For example, if a column person is of type Reference to the People table, then ISREF($person) is True . Similarly, ISREF(People.lookupOne(name=$name)) is True . For any other type of value, ISREF() would evaluate to False . >>> ISREF(17) False >>> ISREF(\"Roger\") False ISREFLIST # ISREFLIST (value) # Checks whether a value is a RecordSet , the type of values in Reference List columns. For example, if a column people is of type Reference List to the People table, then ISREFLIST($people) is True . Similarly, ISREFLIST(People.lookupRecords(name=$name)) is True . For any other type of value, ISREFLIST() would evaluate to False . >>> ISREFLIST(17) False >>> ISREFLIST(\"Roger\") False ISTEXT # ISTEXT (value) # Checks whether a value is text. >>> ISTEXT(\"asdf\") True >>> ISTEXT(\"\") True >>> ISTEXT(AltText(\"text\")) True >>> ISTEXT(17.0) False >>> ISTEXT(None) False >>> ISTEXT(datetime.date(2011, 1, 1)) False ISURL # ISURL (value) # Checks whether a value is a valid URL. It does not need to be fully qualified, or to include \u201chttp://\u201d and \u201cwww\u201d. It does not follow a standard, but attempts to work similarly to ISURL in Google Sheets, and to return True for text that is likely a URL. Valid protocols include ftp, http, https, gopher, mailto, news, telnet, and aim. >>> ISURL(\"http://www.getgrist.com\") True >>> ISURL(\"https://foo.com/test_(wikipedia)#cite-1\") True >>> ISURL(\"mailto://user@example.com\") True >>> ISURL(\"http:///a\") False N # N (value) # Returns the value converted to a number. True/False are converted to 1/0. A date is converted to Excel-style serial number of the date. Anything else is converted to 0. >>> N(7) 7 >>> N(7.1) 7.1 >>> N(\"Even\") 0 >>> N(\"7\") 0 >>> N(True) 1 >>> N(datetime.datetime(2011, 4, 17)) 40650.0 NA # NA () # Returns the \u201cvalue not available\u201d error, #N/A . >>> math.isnan(NA()) True PEEK # PEEK (func) # Evaluates the given expression without creating dependencies or requiring that referenced values are up to date, using whatever value it finds in a cell. This is useful for preventing circular reference errors, particularly in trigger formulas. For example, if the formula for A depends on $B and the formula for B depends on $A , then normally this would raise a circular reference error because each value needs to be calculated before the other. But if A uses PEEK($B) then it will simply get the value already stored in $B without requiring that $B is first calculated to the latest value. Therefore A will be calculated first, and B can use $A without problems. RECORD # RECORD (record_or_list, dates_as_iso=False, expand_refs=0) # Returns a Python dictionary with all fields in the given record. If a list of records is given, returns a list of corresponding Python dictionaries. If dates_as_iso is set, Date and DateTime values are converted to string using ISO 8601 format. If expand_refs is set to 1 or higher, Reference values are replaced with a RECORD representation of the referenced record, expanding the given number of levels. Error values present in cells of the record are replaced with None value, and a special key of \u201c error \u201d gets added containing the error messages for those cells. For example: {\"Ratio\": None, \"_error_\": {\"Ratio\": \"ZeroDivisionError: integer division or modulo by zero\"}} Note that care is needed to avoid circular references when using RECORD(), since it creates a dependency on every cell in the record. In case of RECORD(rec), the cell containing this call will be omitted from the resulting dictionary. For example: RECORD($Person) RECORD(rec) RECORD(People.lookupOne(First_Name=\"Alice\")) RECORD(People.lookupRecords(Department=\"HR\")) REQUEST # REQUEST (url, params=None, headers=None) # Note This function is not currently implemented in Grist. TYPE # TYPE (value) # Returns a number associated with the type of data passed into the function. This is not implemented in Grist. Use isinstance(value, type) or type(value) . Note This function is not currently implemented in Grist. Logical # AND # AND (logical_expression, *logical_expressions) # Returns True if all of the arguments are logically true, and False if any are false. Same as all([value1, value2, ...]) . >>> AND(1) True >>> AND(0) False >>> AND(1, 1) True >>> AND(1,2,3,4) True >>> AND(1,2,3,4,0) False FALSE # FALSE () # Returns the logical value False . You may also use the value False directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> FALSE() False IF # IF (logical_expression, value_if_true, value_if_false) # Returns one value if a logical expression is True and another if it is False . The equivalent Python expression is: value_if_true if logical_expression else value_if_false Since Grist supports multi-line formulas, you may also use Python blocks such as: if logical_expression: return value_if_true else: return value_if_false NOTE: Grist follows Excel model by only evaluating one of the value expressions, by automatically wrapping the expressions to use lazy evaluation. This allows IF(False, 1/0, 1) to evaluate to 1 rather than raise an exception. >>> IF(12, \"Yes\", \"No\") 'Yes' >>> IF(None, \"Yes\", \"No\") 'No' >>> IF(True, 0.85, 0.0) 0.85 >>> IF(False, 0.85, 0.0) 0.0 IFERROR # IFERROR (value, value_if_error=\u2019\u2018) # Returns the first argument if it is not an error value, otherwise returns the second argument if present, or a blank if the second argument is absent. NOTE: Grist handles values that raise an exception by wrapping them to use lazy evaluation. >>> IFERROR(float('nan'), \"**NAN**\") '**NAN**' >>> IFERROR(17.17, \"**NAN**\") 17.17 >>> IFERROR(\"Text\") 'Text' >>> IFERROR(AltText(\"hello\")) '' NOT # NOT (logical_expression) # True . Same as not logical_expression . >>> NOT(123) False >>> NOT(0) True OR # OR (logical_expression, *logical_expressions) # Returns True if any of the arguments is logically true, and false if all of the arguments are false. Same as any([value1, value2, ...]) . >>> OR(1) True >>> OR(0) False >>> OR(1, 1) True >>> OR(0, 1) True >>> OR(0, 0) False >>> OR(0,False,0.0,\"\",None) False >>> OR(0,None,3,0) True TRUE # TRUE () # Returns the logical value True . You may also use the value True directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> TRUE() True Lookup # lookupOne # UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email) lookupRecords # UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. ADDRESS # ADDRESS (row, column, absolute_relative_mode, use_a1_notation, sheet) # Returns a cell reference as a string. Note This function is not currently implemented in Grist. CHOOSE # CHOOSE (index, choice1, choice2) # Returns an element from a list of choices based on index. Note This function is not currently implemented in Grist. COLUMN # COLUMN (cell_reference=None) # Returns the column number of a specified cell, with A=1 . Note This function is not currently implemented in Grist. COLUMNS # COLUMNS (range) # Returns the number of columns in a specified array or range. Note This function is not currently implemented in Grist. CONTAINS # CONTAINS (value, match_empty=no_match_empty) # Use this marker with UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. For example: MoviesTable.lookupRecords(genre=CONTAINS(\"Drama\")) will return records in MoviesTable where the column genre is a list or other container such as [\"Comedy\", \"Drama\"] , i.e. \"Drama\" in $genre . Note that the column being looked up (e.g. genre ) must have values of a container type such as list, tuple, or set. In particular the values mustn\u2019t be strings, e.g. \"Comedy-Drama\" won\u2019t match even though \"Drama\" in \"Comedy-Drama\" is True in Python. It also won\u2019t match substrings within container elements, e.g. [\"Comedy-Drama\"] . You can optionally pass a second argument match_empty to indicate a value that should be matched against empty lists in the looked up column. For example, given this formula: MoviesTable.lookupRecords(genre=CONTAINS(g, match_empty='')) If g is '' (i.e. equal to match_empty ) then the column genre in the returned records will either be an empty list (or other container) or a list containing g as usual. GETPIVOTDATA # GETPIVOTDATA (value_name, any_pivot_table_cell, original_column_1, pivot_item_1=None, *args) # Extracts an aggregated value from a pivot table that corresponds to the specified row and column headings. Note This function is not currently implemented in Grist. HLOOKUP # HLOOKUP (search_key, range, index, is_sorted) # Horizontal lookup. Searches across the first row of a range for a key and returns the value of a specified cell in the column found. Note This function is not currently implemented in Grist. HYPERLINK # HYPERLINK (url, link_label) # Creates a hyperlink inside a cell. Note This function is not currently implemented in Grist. INDEX # INDEX (reference, row, column) # Returns the content of a cell, specified by row and column offset. Note This function is not currently implemented in Grist. INDIRECT # INDIRECT (cell_reference_as_string) # Returns a cell reference specified by a string. Note This function is not currently implemented in Grist. LOOKUP # LOOKUP (search_key, search_range_or_search_result_array, result_range=None) # Looks through a row or column for a key and returns the value of the cell in a result range located in the same position as the search row or column. Note This function is not currently implemented in Grist. MATCH # MATCH (search_key, range, search_type) # Returns the relative position of an item in a range that matches a specified value. Note This function is not currently implemented in Grist. OFFSET # OFFSET (cell_reference, offset_rows, offset_columns, height, width) # Returns a range reference shifted a specified number of rows and columns from a starting cell reference. Note This function is not currently implemented in Grist. ROW # ROW (cell_reference) # Returns the row number of a specified cell. Note This function is not currently implemented in Grist. ROWS # ROWS (range) # Returns the number of rows in a specified array or range. Note This function is not currently implemented in Grist. SELF_HYPERLINK # SELF_HYPERLINK (label=None, page=None, **kwargs) # Creates a link to the current document. All parameters are optional. The returned string is in URL format, optionally preceded by a label and a space (the format expected for Grist Text columns with the HyperLink option enabled). A numeric page number can be supplied, which will create a link to the specified page. To find the numeric page number you need, visit a page and examine its URL for a /p/NN part. Any number of arguments of the form LinkKey_NAME may be provided, to set user.LinkKey.NAME values that will be available in access rules. For example, if a rule allows users to view rows when user.LinkKey.Code == rec.Code , we might want to create links with SELF_HYPERLINK(LinkKey_Code=$Code) . >>> SELF_HYPERLINK() u'https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(label='doc') u'doc https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(page=2) u'https://docs.getgrist.com/sbaltsirg/Example/p/2' >>> SELF_HYPERLINK(LinkKey_Code='X1234') u'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234' >>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo') u'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo' >>> SELF_HYPERLINK(Linky_Link='Link') Traceback (most recent call last): ... TypeError: unexpected keyword argument 'Linky_Link' (not of form LinkKey_NAME) VLOOKUP # VLOOKUP (table, **field_value_pairs) # Vertical lookup. Searches the given table for a record matching the given field=value arguments. If multiple records match, returns one of them. If none match, returns the special empty record. The returned object is a record whose fields are available using .field syntax. For example, VLOOKUP(Employees, EmployeeID=$EmpID).Salary . Note that VLOOKUP isn\u2019t commonly needed in Grist, since Reference columns are the best way to link data between tables, and allow simple efficient usage such as $Person.Age . VLOOKUP is exactly quivalent to table.lookupOne(**field_value_pairs) . See lookupOne . For example: VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\") VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\").Age Math # ABS # ABS (value) # Returns the absolute value of a number. >>> ABS(2) 2 >>> ABS(-2) 2 >>> ABS(-4) 4 ACOS # ACOS (value) # Returns the inverse cosine of a value, in radians. >>> round(ACOS(-0.5), 9) 2.094395102 >>> round(ACOS(-0.5)*180/PI(), 10) 120.0 ACOSH # ACOSH (value) # Returns the inverse hyperbolic cosine of a number. >>> ACOSH(1) 0.0 >>> round(ACOSH(10), 7) 2.9932228 ARABIC # ARABIC (roman_numeral) # Computes the value of a Roman numeral. >>> ARABIC(\"LVII\") 57 >>> ARABIC('mcmxii') 1912 ASIN # ASIN (value) # Returns the inverse sine of a value, in radians. >>> round(ASIN(-0.5), 9) -0.523598776 >>> round(ASIN(-0.5)*180/PI(), 10) -30.0 >>> round(DEGREES(ASIN(-0.5)), 10) -30.0 ASINH # ASINH (value) # Returns the inverse hyperbolic sine of a number. >>> round(ASINH(-2.5), 9) -1.647231146 >>> round(ASINH(10), 9) 2.99822295 ATAN # ATAN (value) # Returns the inverse tangent of a value, in radians. >>> round(ATAN(1), 9) 0.785398163 >>> ATAN(1)*180/PI() 45.0 >>> DEGREES(ATAN(1)) 45.0 ATAN2 # ATAN2 (x, y) # Returns the angle between the x-axis and a line segment from the origin (0,0) to specified coordinate pair ( x , y ), in radians. >>> round(ATAN2(1, 1), 9) 0.785398163 >>> round(ATAN2(-1, -1), 9) -2.35619449 >>> ATAN2(-1, -1)*180/PI() -135.0 >>> DEGREES(ATAN2(-1, -1)) -135.0 >>> round(ATAN2(1,2), 9) 1.107148718 ATANH # ATANH (value) # Returns the inverse hyperbolic tangent of a number. >>> round(ATANH(0.76159416), 9) 1.00000001 >>> round(ATANH(-0.1), 9) -0.100335348 CEILING # CEILING (value, factor=1) # Rounds a number up to the nearest multiple of factor, or the nearest integer if the factor is omitted or 1. >>> CEILING(2.5, 1) 3 >>> CEILING(-2.5, -2) -4 >>> CEILING(-2.5, 2) -2 >>> CEILING(1.5, 0.1) 1.5 >>> CEILING(0.234, 0.01) 0.24 COMBIN # COMBIN (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects. >>> COMBIN(8,2) 28 >>> COMBIN(4,2) 6 >>> COMBIN(10,7) 120 COS # COS (angle) # Returns the cosine of an angle provided in radians. >>> round(COS(1.047), 7) 0.5001711 >>> round(COS(60*PI()/180), 10) 0.5 >>> round(COS(RADIANS(60)), 10) 0.5 COSH # COSH (value) # Returns the hyperbolic cosine of any real number. >>> round(COSH(4), 6) 27.308233 >>> round(COSH(EXP(1)), 7) 7.6101251 DEGREES # DEGREES (angle) # Converts an angle value in radians to degrees. >>> round(DEGREES(ACOS(-0.5)), 10) 120.0 >>> DEGREES(PI()) 180.0 EVEN # EVEN (value) # Rounds a number up to the nearest even integer, rounding away from zero. >>> EVEN(1.5) 2 >>> EVEN(3) 4 >>> EVEN(2) 2 >>> EVEN(-1) -2 EXP # EXP (exponent) # Returns Euler\u2019s number, e (~2.718) raised to a power. >>> round(EXP(1), 8) 2.71828183 >>> round(EXP(2), 7) 7.3890561 FACT # FACT (value) # Returns the factorial of a number. >>> FACT(5) 120 >>> FACT(1.9) 1 >>> FACT(0) 1 >>> FACT(1) 1 >>> FACT(-1) Traceback (most recent call last): ... ValueError: factorial() not defined for negative values FACTDOUBLE # FACTDOUBLE (value) # Returns the \u201cdouble factorial\u201d of a number. >>> FACTDOUBLE(6) 48 >>> FACTDOUBLE(7) 105 >>> FACTDOUBLE(3) 3 >>> FACTDOUBLE(4) 8 FLOOR # FLOOR (value, factor=1) # Rounds a number down to the nearest integer multiple of specified significance. >>> FLOOR(3.7,2) 2 >>> FLOOR(-2.5,-2) -2 >>> FLOOR(2.5,-2) Traceback (most recent call last): ... ValueError: factor argument invalid >>> FLOOR(1.58,0.1) 1.5 >>> FLOOR(0.234,0.01) 0.23 GCD # GCD (value1, *more_values) # Returns the greatest common divisor of one or more integers. >>> GCD(5, 2) 1 >>> GCD(24, 36) 12 >>> GCD(7, 1) 1 >>> GCD(5, 0) 5 >>> GCD(0, 5) 5 >>> GCD(5) 5 >>> GCD(14, 42, 21) 7 INT # INT (value) # Rounds a number down to the nearest integer that is less than or equal to it. >>> INT(8.9) 8 >>> INT(-8.9) -9 >>> 19.5-INT(19.5) 0.5 LCM # LCM (value1, *more_values) # Returns the least common multiple of one or more integers. >>> LCM(5, 2) 10 >>> LCM(24, 36) 72 >>> LCM(0, 5) 0 >>> LCM(5) 5 >>> LCM(10, 100) 100 >>> LCM(12, 18) 36 >>> LCM(12, 18, 24) 72 LN # LN (value) # Returns the the logarithm of a number, base e (Euler\u2019s number). >>> round(LN(86), 7) 4.4543473 >>> round(LN(2.7182818), 7) 1.0 >>> round(LN(EXP(3)), 10) 3.0 LOG # LOG (value, base=10) # Returns the the logarithm of a number given a base. >>> LOG(10) 1.0 >>> LOG(8, 2) 3.0 >>> round(LOG(86, 2.7182818), 7) 4.4543473 LOG10 # LOG10 (value) # Returns the the logarithm of a number, base 10. >>> round(LOG10(86), 9) 1.934498451 >>> LOG10(10) 1.0 >>> LOG10(100000) 5.0 >>> LOG10(10**5) 5.0 MOD # MOD (dividend, divisor) # Returns the result of the modulo operator, the remainder after a division operation. >>> MOD(3, 2) 1 >>> MOD(-3, 2) 1 >>> MOD(3, -2) -1 >>> MOD(-3, -2) -1 MROUND # MROUND (value, factor) # Rounds one number to the nearest integer multiple of another. >>> MROUND(10, 3) 9 >>> MROUND(-10, -3) -9 >>> round(MROUND(1.3, 0.2), 10) 1.4 >>> MROUND(5, -2) Traceback (most recent call last): ... ValueError: factor argument invalid MULTINOMIAL # MULTINOMIAL (value1, *more_values) # Returns the factorial of the sum of values divided by the product of the values\u2019 factorials. >>> MULTINOMIAL(2, 3, 4) 1260 >>> MULTINOMIAL(3) 1 >>> MULTINOMIAL(1,2,3) 60 >>> MULTINOMIAL(0,2,4,6) 13860 ODD # ODD (value) # Rounds a number up to the nearest odd integer. >>> ODD(1.5) 3 >>> ODD(3) 3 >>> ODD(2) 3 >>> ODD(-1) -1 >>> ODD(-2) -3 PI # PI () # Returns the value of Pi to 14 decimal places. >>> round(PI(), 9) 3.141592654 >>> round(PI()/2, 9) 1.570796327 >>> round(PI()*9, 8) 28.27433388 POWER # POWER (base, exponent) # Returns a number raised to a power. >>> POWER(5,2) 25.0 >>> round(POWER(98.6,3.2), 3) 2401077.222 >>> round(POWER(4,5.0/4), 9) 5.656854249 PRODUCT # PRODUCT (factor1, *more_factors) # Returns the result of multiplying a series of numbers together. Each argument may be a number or an array. >>> PRODUCT([5,15,30]) 2250 >>> PRODUCT([5,15,30], 2) 4500 >>> PRODUCT(5,15,[30],[2]) 4500 QUOTIENT # QUOTIENT (dividend, divisor) # Returns one number divided by another, without the remainder. >>> QUOTIENT(5, 2) 2 >>> QUOTIENT(4.5, 3.1) 1 >>> QUOTIENT(-10, 3) -3 RADIANS # RADIANS (angle) # Converts an angle value in degrees to radians. >>> round(RADIANS(270), 6) 4.712389 RAND # RAND () # Returns a random number between 0 inclusive and 1 exclusive. RANDBETWEEN # RANDBETWEEN (low, high) # Returns a uniformly random integer between two values, inclusive. ROMAN # ROMAN (number, form_unused=None) # Formats a number in Roman numerals. The second argument is ignored in this implementation. >>> ROMAN(499,0) 'CDXCIX' >>> ROMAN(499.2,0) 'CDXCIX' >>> ROMAN(57) 'LVII' >>> ROMAN(1912) 'MCMXII' ROUND # ROUND (value, places=0) # Rounds a number to a certain number of decimal places, by default to the nearest whole number if the number of places is not given. Rounds away from zero (\u2018up\u2019 for positive numbers) in the case of a tie, i.e. when the last digit is 5. >>> ROUND(1.4) 1.0 >>> ROUND(1.5) 2.0 >>> ROUND(2.5) 3.0 >>> ROUND(-2.5) -3.0 >>> ROUND(2.15, 1) 2.2 >>> ROUND(-1.475, 2) -1.48 >>> ROUND(21.5, -1) 20.0 >>> ROUND(626.3,-3) 1000.0 >>> ROUND(1.98,-1) 0.0 >>> ROUND(-50.55,-2) -100.0 >>> ROUND(0) 0.0 ROUNDDOWN # ROUNDDOWN (value, places=0) # Rounds a number to a certain number of decimal places, always rounding down towards zero. >>> ROUNDDOWN(3.2, 0) 3 >>> ROUNDDOWN(76.9,0) 76 >>> ROUNDDOWN(3.14159, 3) 3.141 >>> ROUNDDOWN(-3.14159, 1) -3.1 >>> ROUNDDOWN(31415.92654, -2) 31400 ROUNDUP # ROUNDUP (value, places=0) # Rounds a number to a certain number of decimal places, always rounding up away from zero. >>> ROUNDUP(3.2,0) 4 >>> ROUNDUP(76.9,0) 77 >>> ROUNDUP(3.14159, 3) 3.142 >>> ROUNDUP(-3.14159, 1) -3.2 >>> ROUNDUP(31415.92654, -2) 31500 SERIESSUM # SERIESSUM (x, n, m, a) # Given parameters x, n, m, and a, returns the power series sum a_1 x^n + a_2 x^(n+m) + \u2026 + a_i*x^(n+(i-1)m), where i is the number of entries in range a . >>> SERIESSUM(1,0,1,1) 1 >>> SERIESSUM(2,1,0,[1,2,3]) 12 >>> SERIESSUM(-3,1,1,[2,4,6]) -132 >>> round(SERIESSUM(PI()/4,0,2,[1,-1./FACT(2),1./FACT(4),-1./FACT(6)]), 6) 0.707103 SIGN # SIGN (value) # Given an input number, returns -1 if it is negative, 1 if positive, and 0 if it is zero. >>> SIGN(10) 1 >>> SIGN(4.0-4.0) 0 >>> SIGN(-0.00001) -1 SIN # SIN (angle) # Returns the sine of an angle provided in radians. >>> round(SIN(PI()), 10) 0.0 >>> SIN(PI()/2) 1.0 >>> round(SIN(30*PI()/180), 10) 0.5 >>> round(SIN(RADIANS(30)), 10) 0.5 SINH # SINH (value) # Returns the hyperbolic sine of any real number. >>> round(2.868*SINH(0.0342*1.03), 7) 0.1010491 SQRT # SQRT (value) # Returns the positive square root of a positive number. >>> SQRT(16) 4.0 >>> SQRT(-16) Traceback (most recent call last): ... ValueError: math domain error >>> SQRT(ABS(-16)) 4.0 SQRTPI # SQRTPI (value) # Returns the positive square root of the product of Pi and the given positive number. >>> round(SQRTPI(1), 6) 1.772454 >>> round(SQRTPI(2), 6) 2.506628 SUBTOTAL # SUBTOTAL (function_code, range1, range2) # Returns a subtotal for a vertical range of cells using a specified aggregation function. Note This function is not currently implemented in Grist. SUM # SUM (value1, *more_values) # Returns the sum of a series of numbers. Each argument may be a number or an array. Non-numeric values are ignored. >>> SUM([5,15,30]) 50 >>> SUM([5.,15,30], 2) 52.0 >>> SUM(5,15,[30],[2]) 52 SUMIF # SUMIF (records, criterion, sum_range) # Returns a conditional sum across a range. Note This function is not currently implemented in Grist. SUMIFS # SUMIFS (sum_range, criteria_range1, criterion1, *args) # Returns the sum of a range depending on multiple criteria. Note This function is not currently implemented in Grist. SUMPRODUCT # SUMPRODUCT (array1, *more_arrays) # Multiplies corresponding components in two equally-sized arrays, and returns the sum of those products. >>> SUMPRODUCT([3,8,1,4,6,9], [2,6,5,7,7,3]) 156 >>> SUMPRODUCT([], [], []) 0 >>> SUMPRODUCT([-0.25], [-2], [-3]) -1.5 >>> SUMPRODUCT([-0.25, -0.25], [-2, -2], [-3, -3]) -3.0 SUMSQ # SUMSQ (value1, value2) # Returns the sum of the squares of a series of numbers and/or cells. Note This function is not currently implemented in Grist. TAN # TAN (angle) # Returns the tangent of an angle provided in radians. >>> round(TAN(0.785), 8) 0.99920399 >>> round(TAN(45*PI()/180), 10) 1.0 >>> round(TAN(RADIANS(45)), 10) 1.0 TANH # TANH (value) # Returns the hyperbolic tangent of any real number. >>> round(TANH(-2), 6) -0.964028 >>> TANH(0) 0.0 >>> round(TANH(0.5), 6) 0.462117 TRUNC # TRUNC (value, places=0) # Truncates a number to a certain number of significant digits by omitting less significant digits. >>> TRUNC(8.9) 8 >>> TRUNC(-8.9) -8 >>> TRUNC(0.45) 0 UUID # UUID () # Generate a random UUID-formatted string identifier. Since UUID() produces a different value each time it\u2019s called, it is best to use it in trigger formula for new records. This would only calculate UUID() once and freeze the calculated value. By contrast, a regular formula may get recalculated any time the document is reloaded, producing a different value for UUID() each time. Schedule # SCHEDULE # SCHEDULE (schedule, start=None, count=10, end=None) # Returns the list of datetime objects generated according to the schedule string. Starts at start , which defaults to NOW(). Generates at most count results (10 by default). If end is given, stops there. The schedule has the format \u201cINTERVAL: SLOTS, \u2026\u201d. For example: annual: Jan-15, Apr-15, Jul-15 -- Three times a year on given dates at midnight. annual: 1/15, 4/15, 7/15 -- Same as above. monthly: /1 2pm, /15 2pm -- The 1st and the 15th of each month, at 2pm. 3-months: /10, +1m /20 -- Every 3 months on the 10th of month 1, 20th of month 2. weekly: Mo 9am, Tu 9am, Fr 2pm -- Three times a week at specified times. 2-weeks: Mo, +1w Tu -- Every 2 weeks on Monday of week 1, Tuesday of week 2. daily: 07:30, 21:00 -- Twice a day at specified times. 2-day: 12am, 4pm, +1d 8am -- Three times every two days, evenly spaced. hourly: :15, :45 -- 15 minutes before and after each hour. 4-hour: :00, 1:20, 2:40 -- Three times every 4 hours, evenly spaced. 10-minute: +0s -- Every 10 minutes on the minute. INTERVAL must be either of the form N-unit where N is a number and unit is one of year , month , week , day , hour ; or one of the aliases: annual , monthly , weekly , daily , hourly , which mean 1-year , 1-month , etc. SLOTS support the following units: `Jan-15` or `1/15` -- Month and day of the month; available when INTERVAL is year-based. `/15` -- Day of the month, available when INTERVAL is month-based. `Mon`, `Mo`, `Friday` -- Day of the week (or abbreviation), when INTERVAL is week-based. 10am, 1:30pm, 15:45 -- Time of day, available for day-based or longer intervals. :45, :00 -- Minutes of the hour, available when INTERVAL is hour-based. +1d, +15d -- How many days to add to start of INTERVAL. +1w -- How many weeks to add to start of INTERVAL. +1m -- How many months to add to start of INTERVAL. The SLOTS are always relative to the INTERVAL rather than to start . Week-based intervals start on Sunday. E.g. weekly: +1d, +4d is the same as weekly: Mon, Thu , and generates times on Mondays and Thursdays regardless of start . The first generated time is determined by the unit of the INTERVAL without regard to the multiple. E.g. both \u201c2-week: Mon\u201d and \u201c3-week: Mon\u201d start on the first Monday after start , and then generate either every second or every third Monday after that. Similarly, 24-hour: :00 starts with the first top-of-the-hour after start (not with midnight), and then repeats every 24 hours. To start with the midnight after start , use daily: 0:00 . For interval units of a day or longer, if time-of-day is not specified, it defaults to midnight. The time zone of start determines the time zone of the generated times. >>> def show(dates): return [d.strftime(\"%Y-%m-%d %H:%M\") for d in dates] >>> start = datetime(2018, 9, 4, 14, 0); # 2pm on Tue, Sep 4 2018. >>> show(SCHEDULE('annual: Jan-15, Apr-15, Jul-15, Oct-15', start=start, count=4)) ['2018-10-15 00:00', '2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00'] >>> show(SCHEDULE('annual: 1/15, 4/15, 7/15', start=start, count=4)) ['2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00', '2020-01-15 00:00'] >>> show(SCHEDULE('monthly: /1 2pm, /15 5pm', start=start, count=4)) ['2018-09-15 17:00', '2018-10-01 14:00', '2018-10-15 17:00', '2018-11-01 14:00'] >>> show(SCHEDULE('3-months: /10, +1m /20', start=start, count=4)) ['2018-09-10 00:00', '2018-10-20 00:00', '2018-12-10 00:00', '2019-01-20 00:00'] >>> show(SCHEDULE('weekly: Mo 9am, Tu 9am, Fr 2pm', start=start, count=4)) ['2018-09-07 14:00', '2018-09-10 09:00', '2018-09-11 09:00', '2018-09-14 14:00'] >>> show(SCHEDULE('2-weeks: Mo, +1w Tu', start=start, count=4)) ['2018-09-11 00:00', '2018-09-17 00:00', '2018-09-25 00:00', '2018-10-01 00:00'] >>> show(SCHEDULE('daily: 07:30, 21:00', start=start, count=4)) ['2018-09-04 21:00', '2018-09-05 07:30', '2018-09-05 21:00', '2018-09-06 07:30'] >>> show(SCHEDULE('2-day: 12am, 4pm, +1d 8am', start=start, count=4)) ['2018-09-04 16:00', '2018-09-05 08:00', '2018-09-06 00:00', '2018-09-06 16:00'] >>> show(SCHEDULE('hourly: :15, :45', start=start, count=4)) ['2018-09-04 14:15', '2018-09-04 14:45', '2018-09-04 15:15', '2018-09-04 15:45'] >>> show(SCHEDULE('4-hour: :00, +1H :20, +2H :40', start=start, count=4)) ['2018-09-04 14:00', '2018-09-04 15:20', '2018-09-04 16:40', '2018-09-04 18:00'] Stats # AVEDEV # AVEDEV (value1, value2) # Calculates the average of the magnitudes of deviations of data from a dataset\u2019s mean. Note This function is not currently implemented in Grist. AVERAGE # AVERAGE (value, *more_values) # Returns the numerical average value in a dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. >>> AVERAGE([2, -1.0, 11]) 4.0 >>> AVERAGE([2, -1, 11, \"Hello\"]) 4.0 >>> AVERAGE([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11]) 4.0 >>> AVERAGE(False, True) Traceback (most recent call last): ... ZeroDivisionError: float division by zero AVERAGEA # AVERAGEA (value, *more_values) # Returns the numerical average value in a dataset, counting non-numerical values as 0. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. >>> AVERAGEA([2, -1.0, 11]) 4.0 >>> AVERAGEA([2, -1, 11, \"Hello\"]) 3.0 >>> AVERAGEA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 1.5 >>> AVERAGEA(False, True) 0.5 AVERAGEIF # AVERAGEIF (criteria_range, criterion, average_range=None) # Returns the average of a range depending on criteria. Note This function is not currently implemented in Grist. AVERAGEIFS # AVERAGEIFS (average_range, criteria_range1, criterion1, *args) # Returns the average of a range depending on multiple criteria. Note This function is not currently implemented in Grist. AVERAGE_WEIGHTED # AVERAGE_WEIGHTED (pairs) # Given a list of (value, weight) pairs, finds the average of the values weighted by the corresponding weights. Ignores any pairs with a non-numerical value or weight. If you have two lists, of values and weights, use the Python built-in zip() function to create a list of pairs. >>> AVERAGE_WEIGHTED(((95, .25), (90, .1), (\"X\", .5), (85, .15), (88, .2), (82, .3), (70, None))) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, \"X\", 85, 88, 82, 70], [25, 10, 50, 15, 20, 30, None])) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, False, 85, 88, 82, 70], [.25, .1, .5, .15, .2, .3, True])) 87.7 BINOMDIST # BINOMDIST (num_successes, num_trials, prob_success, cumulative) # Calculates the probability of drawing a certain number of successes (or a maximum number of successes) in a certain number of tries given a population of a certain size containing a certain number of successes, with replacement of draws. Note This function is not currently implemented in Grist. CONFIDENCE # CONFIDENCE (alpha, standard_deviation, pop_size) # Calculates the width of half the confidence interval for a normal distribution. Note This function is not currently implemented in Grist. CORREL # CORREL (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist. COUNT # COUNT (value, *more_values) # Returns the count of numerical and date/datetime values in a dataset, ignoring other types of values. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. >>> COUNT([2, -1.0, 11]) 3 >>> COUNT([2, -1, 11, \"Hello\"]) 3 >>> COUNT([DATE(2000, 1, 1), DATE(2000, 1, 2), DATE(2000, 1, 3), \"Hello\"]) 3 >>> COUNT([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 4 >>> COUNT(False, True) 0 COUNTA # COUNTA (value, *more_values) # Returns the count of all values in a dataset, including non-numerical values. Each argument may be a value or an array. >>> COUNTA([2, -1.0, 11]) 3 >>> COUNTA([2, -1, 11, \"Hello\"]) 4 >>> COUNTA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 9 >>> COUNTA(False, True) 2 COVAR # COVAR (data_y, data_x) # Calculates the covariance of a dataset. Note This function is not currently implemented in Grist. CRITBINOM # CRITBINOM (num_trials, prob_success, target_prob) # Calculates the smallest value for which the cumulative binomial distribution is greater than or equal to a specified criteria. Note This function is not currently implemented in Grist. DEVSQ # DEVSQ (value1, value2) # Calculates the sum of squares of deviations based on a sample. Note This function is not currently implemented in Grist. EXPONDIST # EXPONDIST (x, lambda_, cumulative) # Returns the value of the exponential distribution function with a specified lambda at a specified value. Note This function is not currently implemented in Grist. FDIST # FDIST (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist. FISHER # FISHER (value) # Returns the Fisher transformation of a specified value. Note This function is not currently implemented in Grist. FISHERINV # FISHERINV (value) # Returns the inverse Fisher transformation of a specified value. Note This function is not currently implemented in Grist. FORECAST # FORECAST (x, data_y, data_x) # Calculates the expected y-value for a specified x based on a linear regression of a dataset. Note This function is not currently implemented in Grist. F_DIST # F_DIST (x, degrees_freedom1, degrees_freedom2, cumulative) # Calculates the left-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist. F_DIST_RT # F_DIST_RT (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist. GEOMEAN # GEOMEAN (value1, value2) # Calculates the geometric mean of a dataset. Note This function is not currently implemented in Grist. HARMEAN # HARMEAN (value1, value2) # Calculates the harmonic mean of a dataset. Note This function is not currently implemented in Grist. HYPGEOMDIST # HYPGEOMDIST (num_successes, num_draws, successes_in_pop, pop_size) # Calculates the probability of drawing a certain number of successes in a certain number of tries given a population of a certain size containing a certain number of successes, without replacement of draws. Note This function is not currently implemented in Grist. INTERCEPT # INTERCEPT (data_y, data_x) # Calculates the y-value at which the line resulting from linear regression of a dataset will intersect the y-axis (x=0). Note This function is not currently implemented in Grist. KURT # KURT (value1, value2) # Calculates the kurtosis of a dataset, which describes the shape, and in particular the \u201cpeakedness\u201d of that dataset. Note This function is not currently implemented in Grist. LARGE # LARGE (data, n) # Returns the nth largest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist. LOGINV # LOGINV (x, mean, standard_deviation) # Returns the value of the inverse log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist. LOGNORMDIST # LOGNORMDIST (x, mean, standard_deviation) # Returns the value of the log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist. MAX # MAX (value, *more_values) # Returns the maximum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MAX([2, -1.5, 11.5]) 11.5 >>> MAX([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAX(True, -123) -123 >>> MAX(\"123\", -123) -123 >>> MAX(\"Hello\", \"123\", True, False) 0 >>> MAX(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 2) >>> MAX(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56) >>> MAX(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 2) MAXA # MAXA (value, *more_values) # Returns the maximum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MAXA([2, -1.5, 11.5]) 11.5 >>> MAXA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAXA(True, -123) 1 >>> MAXA(\"123\", -123) 0 >>> MAXA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0 MEDIAN # MEDIAN (value, *more_values) # Returns the median value in a numeric dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. Produces an error if the arguments contain no numbers. The median is the middle number when all values are sorted. So half of the values in the dataset are less than the median, and half of the values are greater. If there is an even number of values in the dataset, returns the average of the two numbers in the middle. >>> MEDIAN(1, 2, 3, 4, 5) 3 >>> MEDIAN(3, 5, 1, 4, 2) 3 >>> MEDIAN(range(10)) 4.5 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1), 12.3) 12.3 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1)) Traceback (most recent call last): ... ValueError: MEDIAN requires at least one number MIN # MIN (value, *more_values) # Returns the minimum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MIN([2, -1.5, 11.5]) -1.5 >>> MIN([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MIN(True, 123) 123 >>> MIN(\"-123\", 123) 123 >>> MIN(\"Hello\", \"123\", True, False) 0 >>> MIN(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56) MINA # MINA (value, *more_values) # Returns the minimum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MINA([2, -1.5, 11.5]) -1.5 >>> MINA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MINA(True, 123) 1 >>> MINA(\"-123\", 123) 0 >>> MINA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0 MODE # MODE (value1, value2) # Returns the most commonly occurring value in a dataset. Note This function is not currently implemented in Grist. NEGBINOMDIST # NEGBINOMDIST (num_failures, num_successes, prob_success) # Calculates the probability of drawing a certain number of failures before a certain number of successes given a probability of success in independent trials. Note This function is not currently implemented in Grist. NORMDIST # NORMDIST (x, mean, standard_deviation, cumulative) # Returns the value of the normal distribution function (or normal cumulative distribution function) for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist. NORMINV # NORMINV (x, mean, standard_deviation) # Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist. NORMSDIST # NORMSDIST (x) # Returns the value of the standard normal cumulative distribution function for a specified value. Note This function is not currently implemented in Grist. NORMSINV # NORMSINV (x) # Returns the value of the inverse standard normal distribution function for a specified value. Note This function is not currently implemented in Grist. PEARSON # PEARSON (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist. PERCENTILE # PERCENTILE (data, percentile) # Returns the value at a given percentile of a dataset. Note This function is not currently implemented in Grist. PERCENTRANK # PERCENTRANK (data, value, significant_digits=None) # Returns the percentage rank (percentile) of a specified value in a dataset. Note This function is not currently implemented in Grist. PERCENTRANK_EXC # PERCENTRANK_EXC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 exclusive of a specified value in a dataset. Note This function is not currently implemented in Grist. PERCENTRANK_INC # PERCENTRANK_INC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 inclusive of a specified value in a dataset. Note This function is not currently implemented in Grist. PERMUT # PERMUT (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects, considering order. Note This function is not currently implemented in Grist. POISSON # POISSON (x, mean, cumulative) # Returns the value of the Poisson distribution function (or Poisson cumulative distribution function) for a specified value and mean. Note This function is not currently implemented in Grist. PROB # PROB (data, probabilities, low_limit, high_limit=None) # Given a set of values and corresponding probabilities, calculates the probability that a value chosen at random falls between two limits. Note This function is not currently implemented in Grist. QUARTILE # QUARTILE (data, quartile_number) # Returns a value nearest to a specified quartile of a dataset. Note This function is not currently implemented in Grist. RANK # RANK (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. Note This function is not currently implemented in Grist. RANK_AVG # RANK_AVG (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the average rank of the entries will be returned. Note This function is not currently implemented in Grist. RANK_EQ # RANK_EQ (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the top rank of the entries will be returned. Note This function is not currently implemented in Grist. RSQ # RSQ (data_y, data_x) # Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist. SKEW # SKEW (value1, value2) # Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean. Note This function is not currently implemented in Grist. SLOPE # SLOPE (data_y, data_x) # Calculates the slope of the line resulting from linear regression of a dataset. Note This function is not currently implemented in Grist. SMALL # SMALL (data, n) # Returns the nth smallest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist. STANDARDIZE # STANDARDIZE (value, mean, standard_deviation) # Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution. Note This function is not currently implemented in Grist. STDEV # STDEV (value, *more_values) # Calculates the standard deviation based on a sample, ignoring non-numerical values. >>> STDEV([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10], 3, 12, 15) 4.810702354423639 >>> STDEV([2, 5, 8, 13, 10], [3, 12, 15]) 4.810702354423639 >>> STDEV([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero STDEVA # STDEVA (value, *more_values) # Calculates the standard deviation based on a sample, setting text to the value 0 . >>> STDEVA([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEVA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], 1, 0, 0) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], [1, 0, 0]) 4.969550137731641 >>> STDEVA([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero STDEVP # STDEVP (value, *more_values) # Calculates the standard deviation based on an entire population, ignoring non-numerical values. >>> STDEVP([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10, True, False, \"Test\"]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10], 3, 12, 15) 4.5 >>> STDEVP([2, 5, 8, 13, 10], [3, 12, 15]) 4.5 >>> STDEVP([5]) 0.0 STDEVPA # STDEVPA (value, *more_values) # Calculates the standard deviation based on an entire population, setting text to the value 0 . >>> STDEVPA([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVPA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], 1, 0, 0) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], [1, 0, 0]) 4.648588495446763 >>> STDEVPA([5]) 0.0 STEYX # STEYX (data_y, data_x) # Calculates the standard error of the predicted y-value for each x in the regression of a dataset. Note This function is not currently implemented in Grist. TDIST # TDIST (x, degrees_freedom, tails) # Calculates the probability for Student\u2019s t-distribution with a given input (x). Note This function is not currently implemented in Grist. TINV # TINV (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist. TRIMMEAN # TRIMMEAN (data, exclude_proportion) # Calculates the mean of a dataset excluding some proportion of data from the high and low ends of the dataset. Note This function is not currently implemented in Grist. TTEST # TTEST (range1, range2, tails, type) # Returns the probability associated with t-test. Determines whether two samples are likely to have come from the same two underlying populations that have the same mean. Note This function is not currently implemented in Grist. T_INV # T_INV (probability, degrees_freedom) # Calculates the negative inverse of the one-tailed TDIST function. Note This function is not currently implemented in Grist. T_INV_2T # T_INV_2T (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist. VAR # VAR (value1, value2) # Calculates the variance based on a sample. Note This function is not currently implemented in Grist. VARA # VARA (value1, value2) # Calculates an estimate of variance based on a sample, setting text to the value 0 . Note This function is not currently implemented in Grist. VARP # VARP (value1, value2) # Calculates the variance based on an entire population. Note This function is not currently implemented in Grist. VARPA # VARPA (value1, value2) # Calculates the variance based on an entire population, setting text to the value 0 . Note This function is not currently implemented in Grist. WEIBULL # WEIBULL (x, shape, scale, cumulative) # Returns the value of the Weibull distribution function (or Weibull cumulative distribution function) for a specified shape and scale. Note This function is not currently implemented in Grist. ZTEST # ZTEST (data, value, standard_deviation) # Returns the two-tailed P-value of a Z-test with standard distribution. Note This function is not currently implemented in Grist. Text # CHAR # CHAR (table_number) # Convert a number into a character according to the current Unicode table. Same as unichr(number) . >>> CHAR(65) u'A' >>> CHAR(33) u'!' CLEAN # CLEAN (text) # Returns the text with the non-printable characters removed. This removes both characters with values 0 through 31, and other Unicode characters in the \u201ccontrol characters\u201d category. >>> CLEAN(CHAR(9) + \"Monthly report\" + CHAR(10)) u'Monthly report' CODE # CODE (string) # Returns the numeric Unicode map value of the first character in the string provided. Same as ord(string[0]) . >>> CODE(\"A\") 65 >>> CODE(\"!\") 33 >>> CODE(\"!A\") 33 CONCAT # CONCAT (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCATENATE . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCAT(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCAT(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCAT(\"abc\") u'abc' >>> CONCAT(0, \"abc\") u'0abc' >>> assert CONCAT(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' CONCATENATE # CONCATENATE (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCAT . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCATENATE(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCATENATE(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCATENATE(\"abc\") u'abc' >>> CONCATENATE(0, \"abc\") u'0abc' >>> assert CONCATENATE(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", \"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' DOLLAR # DOLLAR (number, decimals=2) # Formats a number into a formatted dollar amount, with decimals rounded to the specified place (. If decimals value is omitted, it defaults to 2. >>> DOLLAR(1234.567) '$1,234.57' >>> DOLLAR(1234.567, -2) '$1,200' >>> DOLLAR(-1234.567, -2) '($1,200)' >>> DOLLAR(-0.123, 4) '($0.1230)' >>> DOLLAR(99.888) '$99.89' >>> DOLLAR(0) '$0.00' >>> DOLLAR(10, 0) '$10' EXACT # EXACT (string1, string2) # Tests whether two strings are identical. Same as string2 == string2 . >>> EXACT(\"word\", \"word\") True >>> EXACT(\"Word\", \"word\") False >>> EXACT(\"w ord\", \"word\") False FIND # FIND (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> FIND(\"M\", \"Miriam McGovern\") 1 >>> FIND(\"m\", \"Miriam McGovern\") 6 >>> FIND(\"M\", \"Miriam McGovern\", 3) 8 >>> FIND(\" #\", \"Hello world # Test\") 12 >>> FIND(\"gle\", \"Google\", 1) 4 >>> FIND(\"GLE\", \"Google\", 1) Traceback (most recent call last): ... ValueError: substring not found >>> FIND(\"page\", \"homepage\") 5 >>> FIND(\"page\", \"homepage\", 6) Traceback (most recent call last): ... ValueError: substring not found FIXED # FIXED (number, decimals=2, no_commas=False) # Formats a number with a fixed number of decimal places (2 by default), and commas. If no_commas is True, then omits the commas. >>> FIXED(1234.567, 1) '1,234.6' >>> FIXED(1234.567, -1) '1,230' >>> FIXED(-1234.567, -1, True) '-1230' >>> FIXED(44.332) '44.33' >>> FIXED(3521.478, 2, False) '3,521.48' >>> FIXED(-3521.478, 1, True) '-3521.5' >>> FIXED(3521.478, 0, True) '3521' >>> FIXED(3521.478, -2, True) '3500' LEFT # LEFT (string, num_chars=1) # Returns a substring of length num_chars from the beginning of the given string. If num_chars is omitted, it is assumed to be 1. Same as string[:num_chars] . >>> LEFT(\"Sale Price\", 4) 'Sale' >>> LEFT('Swededn') 'S' >>> LEFT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid LEN # LEN (text) # Returns the number of characters in a text string, or the number of items in a list. Same as len in python. See Record Set for an example of using len on a list of records. >>> LEN(\"Phoenix, AZ\") 11 >>> LEN(\"\") 0 >>> LEN(\" One \") 11 LOWER # LOWER (text) # Converts a specified string to lowercase. Same as text.lower() . >>> LOWER(\"E. E. Cummings\") 'e. e. cummings' >>> LOWER(\"Apt. 2B\") 'apt. 2b' MID # MID (text, start_num, num_chars) # Returns a segment of a string, starting at start_num. The first character in text has start_num 1. >>> MID(\"Fluid Flow\", 1, 5) 'Fluid' >>> MID(\"Fluid Flow\", 7, 20) 'Flow' >>> MID(\"Fluid Flow\", 20, 5) '' >>> MID(\"Fluid Flow\", 0, 5) Traceback (most recent call last): ... ValueError: start_num invalid PHONE_FORMAT # PHONE_FORMAT (value, country=None, format=None) # Formats a phone number. With no optional arguments, the number must start with \u201c+\u201d and the international dialing prefix, and will be formatted as an international number, e.g. +12345678901 becomes +1 234-567-8901 . The country argument allows specifying a 2-letter country code (e.g. \u201cUS\u201d or \u201cGB\u201d) for interpreting phone numbers that don\u2019t start with \u201c+\u201d. E.g. PHONE_FORMAT('2025555555', 'US') would be seen as a US number and formatted as \u201c(202) 555-5555\u201d. Phone numbers that start with \u201c+\u201d ignore country . E.g. PHONE_FORMAT('+33555555555', 'US') is a French number because \u2018+33\u2019 is the international prefix for France. The format argument specifies the output format, according to this table: \"#\" or \"NATL\" (default) - use the national format, without the international dialing prefix, when possible. E.g. (234) 567-8901 for \u201cUS\u201d, or 02 34 56 78 90 for \u201cFR\u201d. If country is omitted, or the number does not correspond to the given country, the international format is used instead. \"+\" or \"INTL\" - international format, e.g. +1 234-567-8901 or +33 2 34 56 78 90 . \"*\" or \"E164\" - E164 format, like international but with no separators, e.g. +12345678901 . \"tel\" or \"RFC3966\" - format suitable to use as a hyperlink , e.g. \u2018tel:+1-234-567-8901\u2019. When specifying the format argument, you may omit the country argument. I.e. PHONE_FORMAT(value, \"tel\") is equivalent to PHONE_FORMAT(value, None, \"tel\") . For more details, see the phonenumbers Python library, which underlies this function. >>> PHONE_FORMAT(\"+12345678901\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"2345678901\", \"US\") u'(234) 567-8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\", \"+\") u'+44 23 4567 8901' >>> PHONE_FORMAT(\"+442345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"+12345678901\", \"GB\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"(234) 567-8901\") Traceback (most recent call last): ... NumberParseException: (0) Missing or invalid default region. >>> PHONE_FORMAT(\"(234)567 89-01\", \"US\", \"tel\") u'tel:+1-234-567-8901' >>> PHONE_FORMAT(\"2/3456/7890\", \"FR\", '#') u'02 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", '#') u'+33 2 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", 'tel') u'tel:+33-2-34-56-78-90' >>> PHONE_FORMAT(\"tel:+1-234-567-8901\", country=\"US\", format=\"*\") u'+12345678901' PROPER # PROPER (text) # Capitalizes each word in a specified string. It converts the first letter of each word to uppercase, and all other letters to lowercase. Same as text.title() . >>> PROPER('this is a TITLE') 'This Is A Title' >>> PROPER('2-way street') '2-Way Street' >>> PROPER('76BudGet') '76Budget' REGEXEXTRACT # REGEXEXTRACT (text, regular_expression) # Extracts the first part of text that matches regular_expression. >>> REGEXEXTRACT(\"Google Doc 101\", \"[0-9]+\") '101' >>> REGEXEXTRACT(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") '826.25' If there is a parenthesized expression, it is returned instead of the whole match. >>> REGEXEXTRACT(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") 'Content' >>> REGEXEXTRACT(\"Foo\", \"Bar\") Traceback (most recent call last): ... ValueError: REGEXEXTRACT text does not match REGEXMATCH # REGEXMATCH (text, regular_expression) # Returns whether a piece of text matches a regular expression. >>> REGEXMATCH(\"Google Doc 101\", \"[0-9]+\") True >>> REGEXMATCH(\"Google Doc\", \"[0-9]+\") False >>> REGEXMATCH(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") True >>> REGEXMATCH(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") True >>> REGEXMATCH(\"Foo\", \"Bar\") False REGEXREPLACE # REGEXREPLACE (text, regular_expression, replacement) # Replaces all parts of text matching the given regular expression with replacement text. >>> REGEXREPLACE(\"Google Doc 101\", \"[0-9]+\", \"777\") 'Google Doc 777' >>> REGEXREPLACE(\"Google Doc\", \"[0-9]+\", \"777\") 'Google Doc' >>> REGEXREPLACE(\"The price is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\", \"315.75\") 'The price is $315.75' >>> REGEXREPLACE(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\", \"Word\") 'Word between brackets' >>> REGEXREPLACE(\"Foo\", \"Bar\", \"Baz\") 'Foo' REPLACE # REPLACE (text, position, length, new_text) # Replaces part of a text string with a different text string. Position is counted from 1. >>> REPLACE(\"abcdefghijk\", 6, 5, \"*\") 'abcde*k' >>> REPLACE(\"2009\", 3, 2, \"10\") '2010' >>> REPLACE('123456', 1, 3, '@') '@456' >>> REPLACE('foo', 1, 0, 'bar') 'barfoo' >>> REPLACE('foo', 0, 1, 'bar') Traceback (most recent call last): ... ValueError: position invalid REPT # REPT (text, number_times) # Returns specified text repeated a number of times. Same as text * number_times . The result of the REPT function cannot be longer than 32767 characters, or it raises a ValueError. >>> REPT(\"*-\", 3) '*-*-*-' >>> REPT('-', 10) '----------' >>> REPT('-', 0) '' >>> len(REPT('---', 10000)) 30000 >>> REPT('---', 11000) Traceback (most recent call last): ... ValueError: number_times invalid >>> REPT('-', -1) Traceback (most recent call last): ... ValueError: number_times invalid RIGHT # RIGHT (string, num_chars=1) # Returns a substring of length num_chars from the end of a specified string. If num_chars is omitted, it is assumed to be 1. Same as string[-num_chars:] . >>> RIGHT(\"Sale Price\", 5) 'Price' >>> RIGHT('Stock Number') 'r' >>> RIGHT('Text', 100) 'Text' >>> RIGHT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid SEARCH # SEARCH (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text, ignoring case. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> SEARCH(\"e\", \"Statements\", 6) 7 >>> SEARCH(\"margin\", \"Profit Margin\") 8 >>> SEARCH(\" \", \"Profit Margin\") 7 >>> SEARCH('\"', 'The \"boss\" is here.') 5 >>> SEARCH(\"gle\", \"Google\") 4 >>> SEARCH(\"GLE\", \"Google\") 4 SUBSTITUTE # SUBSTITUTE (text, old_text, new_text, instance_num=None) # Replaces existing text with new text in a string. It is useful when you know the substring of text to replace. Use REPLACE when you know the position of text to replace. If instance_num is given, it specifies which occurrence of old_text to replace. If omitted, all occurrences are replaced. Same as text.replace(old_text, new_text) when instance_num is omitted. >>> SUBSTITUTE(\"Sales Data\", \"Sales\", \"Cost\") u'Cost Data' >>> SUBSTITUTE(\"Quarter 1, 2008\", \"1\", \"2\", 1) u'Quarter 2, 2008' >>> SUBSTITUTE(\"Quarter 1, 2011\", \"1\", \"2\", 3) u'Quarter 1, 2012' T # T (value) # Returns value if value is text, or the empty string when value is not text. >>> T('Text') u'Text' >>> T(826) u'' >>> T('826') u'826' >>> T(False) u'' >>> T('100 points') u'100 points' >>> T(AltText('Text')) u'Text' >>> T(float('nan')) u'' TEXT # TEXT (number, format_type) # Converts a number into text according to a specified format. It is not yet implemented in Grist. Note This function is not currently implemented in Grist. TRIM # TRIM (text) # Removes all spaces from text except for single spaces between words. Note that TRIM does not remove other whitespace such as tab or newline characters. >>> TRIM(\" First Quarter\\n Earnings \") 'First Quarter\\n Earnings' >>> TRIM(\"\") '' UPPER # UPPER (text) # Converts a specified string to uppercase. Same as text.upper() . >>> UPPER(\"e. e. cummings\") 'E. E. CUMMINGS' >>> UPPER(\"Apt. 2B\") 'APT. 2B' VALUE # VALUE (text) # Converts a string in accepted date, time or number formats into a number or date. >>> VALUE(\"$1,000\") 1000 >>> assert VALUE(\"16:48:00\") - VALUE(\"12:00:00\") == datetime.timedelta(0, 17280) >>> VALUE(\"01/01/2012\") datetime.datetime(2012, 1, 1, 0, 0) >>> VALUE(\"\") 0 >>> VALUE(0) 0 >>> VALUE(\"826\") 826 >>> VALUE(\"-826.123123123\") -826.123123123 >>> VALUE(float('nan')) nan >>> VALUE(\"Invalid\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number >>> VALUE(\"13/13/13\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number","title":"Function reference"},{"location":"functions/#function-reference","text":"Grist formulas support most Excel functions, as well as the Python programming language. The table below lists Grist-specific functions, and the suite of the included Excel-like functions. In addition, the entire Python standard library is available. For more about using formulas in Grist, see Intro to Formulas . Grist uses Python (version 3.11) for formulas. You can use nearly all features of Python (see Python documentation ). Here are some helpful notes: Python is case-sensitive, including for Grist table and column names. Excel-like functions are always in uppercase. E.g. if is a Python keyword, while IF is an Excel-like function. Compare for equality using == , in place of Excel\u2019s single = (which in Python means assignment). \u201cNot equal\u201d uses != in place of Excel\u2019s <> . You may write multi-line Python in formulas (use Shift + Enter to add lines), including statements, variables, imports, etc. Grist code runs in a secure sandbox, with no access to anything outside your document. Category Functions Grist Record or rec , $Field or rec.Field , $group or rec.group , RecordSet , UserTable , all , lookupOne , lookupRecords Date DATE , DATEADD , DATEDIF , DATEVALUE , DATE_TO_XL , DAY , DAYS , DTIME , EDATE , EOMONTH , HOUR , ISOWEEKNUM , MINUTE , MONTH , NOW , SECOND , TODAY , WEEKDAY , WEEKNUM , XL_TO_DATE , YEAR , YEARFRAC Info CELL , CURRENT_CONVERSION , ISBLANK , ISEMAIL , ISERR , ISERROR , ISLOGICAL , ISNA , ISNONTEXT , ISNUMBER , ISREF , ISREFLIST , ISTEXT , ISURL , N , NA , PEEK , RECORD , REQUEST , TYPE Logical AND , FALSE , IF , IFERROR , NOT , OR , TRUE Lookup lookupOne , lookupRecords , ADDRESS , CHOOSE , COLUMN , COLUMNS , CONTAINS , GETPIVOTDATA , HLOOKUP , HYPERLINK , INDEX , INDIRECT , LOOKUP , MATCH , OFFSET , ROW , ROWS , SELF_HYPERLINK , VLOOKUP Math ABS , ACOS , ACOSH , ARABIC , ASIN , ASINH , ATAN , ATAN2 , ATANH , CEILING , COMBIN , COS , COSH , DEGREES , EVEN , EXP , FACT , FACTDOUBLE , FLOOR , GCD , INT , LCM , LN , LOG , LOG10 , MOD , MROUND , MULTINOMIAL , ODD , PI , POWER , PRODUCT , QUOTIENT , RADIANS , RAND , RANDBETWEEN , ROMAN , ROUND , ROUNDDOWN , ROUNDUP , SERIESSUM , SIGN , SIN , SINH , SQRT , SQRTPI , SUBTOTAL , SUM , SUMIF , SUMIFS , SUMPRODUCT , SUMSQ , TAN , TANH , TRUNC , UUID Schedule SCHEDULE Stats AVEDEV , AVERAGE , AVERAGEA , AVERAGEIF , AVERAGEIFS , AVERAGE_WEIGHTED , BINOMDIST , CONFIDENCE , CORREL , COUNT , COUNTA , COVAR , CRITBINOM , DEVSQ , EXPONDIST , FDIST , FISHER , FISHERINV , FORECAST , F_DIST , F_DIST_RT , GEOMEAN , HARMEAN , HYPGEOMDIST , INTERCEPT , KURT , LARGE , LOGINV , LOGNORMDIST , MAX , MAXA , MEDIAN , MIN , MINA , MODE , NEGBINOMDIST , NORMDIST , NORMINV , NORMSDIST , NORMSINV , PEARSON , PERCENTILE , PERCENTRANK , PERCENTRANK_EXC , PERCENTRANK_INC , PERMUT , POISSON , PROB , QUARTILE , RANK , RANK_AVG , RANK_EQ , RSQ , SKEW , SLOPE , SMALL , STANDARDIZE , STDEV , STDEVA , STDEVP , STDEVPA , STEYX , TDIST , TINV , TRIMMEAN , TTEST , T_INV , T_INV_2T , VAR , VARA , VARP , VARPA , WEIBULL , ZTEST Text CHAR , CLEAN , CODE , CONCAT , CONCATENATE , DOLLAR , EXACT , FIND , FIXED , LEFT , LEN , LOWER , MID , PHONE_FORMAT , PROPER , REGEXEXTRACT , REGEXMATCH , REGEXREPLACE , REPLACE , REPT , RIGHT , SEARCH , SUBSTITUTE , T , TEXT , TRIM , UPPER , VALUE","title":""},{"location":"functions/#grist","text":"","title":"Grist"},{"location":"functions/#record","text":"class Record # A Record represents a record of data. It is the primary means of accessing values in formulas. A Record for a particular table has a property for each data and formula column in the table. In a formula, $field is translated to rec.field , where rec is the Record for which the formula is being evaluated. For example: def Full_Name(rec, table): return rec.First_Name + ' ' + rec.LastName def Name_Length(rec, table): return len(rec.Full_Name)","title":"Record"},{"location":"functions/#field","text":"$ Field or rec .Field # Access the field named \u201cField\u201d of the current record. E.g. $First_Name or rec.First_Name .","title":"$Field"},{"location":"functions/#group","text":"$group # In a summary table , $group is a special field containing the list of Records that are summarized by the current summary line. E.g. the formula len($group) counts the number of those records being summarized in each row. See RecordSet for useful properties offered by the returned object. Examples: sum($group.Amount) # Sum of the Amount field in the matching records sum(r.Amount for r in $group) # Same as sum($group.Amount) sum(r.Amount for r in $group if r > 0) # Sum of only the positive amounts sum(r.Shares * r.Price for r in $group) # Sum of shares * price products","title":"$group"},{"location":"functions/#recordset","text":"class RecordSet # A RecordSet represents a collection of records, as returned by Table.lookupRecords() or $group property in summary views. A RecordSet allows iterating through the records: sum(r.Amount for r in Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\")) min(r.DueDate for r in Tasks.lookupRecords(Owner=\"Bob\")) RecordSets also provide a convenient way to access the list of values for a particular field for all the records, as record_set.Field . For example, the examples above are equivalent to: sum(Students.lookupRecords(First_Name=\"John\", Last_Name=\"Doe\").Amount) min(Tasks.lookupRecords(Owner=\"Bob\").DueDate) You can get the number of records in a RecordSet using len , e.g. len($group) .","title":"RecordSet"},{"location":"functions/#usertable","text":"class UserTable # Each data table in the document is represented in the code by an instance of UserTable class. These names are always capitalized. A UserTable provides access to all the records in the table, as well as methods to look up particular records. Every table in the document is available to all formulas.","title":"UserTable"},{"location":"functions/#all","text":"UserTable. all # The list of all the records in this table. For example, this evaluates to the number of records in the table Students . len(Students.all) This evaluates to the sum of the Population field for every record in the table Countries . sum(r.Population for r in Countries.all)","title":"all"},{"location":"functions/#lookupone","text":"UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. If sort_by=field is given, sort the results by that field. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email) People.lookupOne(Email=$Work_Email, sort_by=\"Date\")","title":"lookupOne"},{"location":"functions/#lookuprecords","text":"UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value.","title":"lookupRecords"},{"location":"functions/#date","text":"","title":"Date"},{"location":"functions/#date_1","text":"DATE (year, month, day) # Returns the datetime.datetime object that represents a particular date. The DATE function is most useful in formulas where year, month, and day are formulas, not constants. If year is between 0 and 1899 (inclusive), adds 1900 to calculate the year. >>> DATE(108, 1, 2) datetime.date(2008, 1, 2) >>> DATE(2008, 1, 2) datetime.date(2008, 1, 2) If month is greater than 12, rolls into the following year. >>> DATE(2008, 14, 2) datetime.date(2009, 2, 2) If month is less than 1, subtracts that many months plus 1, from the first month in the year. >>> DATE(2008, -3, 2) datetime.date(2007, 9, 2) If day is greater than the number of days in the given month, rolls into the following months. >>> DATE(2008, 1, 35) datetime.date(2008, 2, 4) If day is less than 1, subtracts that many days plus 1, from the first day of the given month. >>> DATE(2008, 1, -15) datetime.date(2007, 12, 16)","title":"DATE"},{"location":"functions/#dateadd","text":"DATEADD (start_date, days=0, months=0, years=0, weeks=0) # Returns the date a given number of days, months, years, or weeks away from start_date . You may specify arguments in any order if you specify argument names. Use negative values to subtract. For example, DATEADD(date, 1) is the same as DATEADD(date, days=1) , ands adds one day to date . DATEADD(date, years=1, days=-1) adds one year minus one day. >>> DATEADD(DATE(2011, 1, 15), 1) datetime.date(2011, 1, 16) >>> DATEADD(DATE(2011, 1, 15), months=1, days=-1) datetime.date(2011, 2, 14) >>> DATEADD(DATE(2011, 1, 15), years=-2, months=1, days=3, weeks=2) datetime.date(2009, 3, 4) >>> DATEADD(DATE(1975, 4, 30), years=50, weeks=-5) datetime.date(2025, 3, 26)","title":"DATEADD"},{"location":"functions/#datedif","text":"DATEDIF (start_date, end_date, unit) # Calculates the number of days, months, or years between two dates. Unit indicates the type of information that you want returned: \u201cY\u201d: The number of complete years in the period. \u201cM\u201d: The number of complete months in the period. \u201cD\u201d: The number of days in the period. \u201cMD\u201d: The difference between the days in start_date and end_date. The months and years of the dates are ignored. \u201cYM\u201d: The difference between the months in start_date and end_date. The days and years of the dates are ignored. \u201cYD\u201d: The difference between the days of start_date and end_date. The years of the dates are ignored. Two complete years in the period (2) >>> DATEDIF(DATE(2001, 1, 1), DATE(2003, 1, 1), \"Y\") 2 440 days between June 1, 2001, and August 15, 2002 (440) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"D\") 440 75 days between June 1 and August 15, ignoring the years of the dates (75) >>> DATEDIF(DATE(2001, 6, 1), DATE(2012, 8, 15), \"YD\") 75 The difference between 1 and 15, ignoring the months and the years of the dates (14) >>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), \"MD\") 14","title":"DATEDIF"},{"location":"functions/#datevalue","text":"DATEVALUE (date_string, tz=None) # Converts a date that is stored as text to a datetime object. >>> DATEVALUE(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"30-Jan-2008\") datetime.datetime(2008, 1, 30, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"2008-12-11\") datetime.datetime(2008, 12, 11, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DATEVALUE(\"5-JUL\").replace(year=2000) datetime.datetime(2000, 7, 5, 0, 0, tzinfo=moment.tzinfo('America/New_York')) In case of ambiguity, prefer M/D/Y format. >>> DATEVALUE(\"1/2/3\") datetime.datetime(2003, 1, 2, 0, 0, tzinfo=moment.tzinfo('America/New_York'))","title":"DATEVALUE"},{"location":"functions/#date_to_xl","text":"DATE_TO_XL (date_value) # Converts a Python date or datetime object to the serial number as used by Excel, with December 30, 1899 as serial number 1. See XL_TO_DATE for more explanation. >>> DATE_TO_XL(datetime.date(2008, 1, 1)) 39448.0 >>> DATE_TO_XL(datetime.date(2012, 3, 14)) 40982.0 >>> DATE_TO_XL(datetime.datetime(2012, 3, 14, 1, 30)) 40982.0625","title":"DATE_TO_XL"},{"location":"functions/#day","text":"DAY (date) # Returns the day of a date, as an integer ranging from 1 to 31. Same as date.day . >>> DAY(DATE(2011, 4, 15)) 15 >>> DAY(\"5/31/2012\") 31 >>> DAY(datetime.datetime(1900, 1, 1)) 1","title":"DAY"},{"location":"functions/#days","text":"DAYS (end_date, start_date) # Returns the number of days between two dates. Same as (end_date - start_date).days . >>> DAYS(\"3/15/11\",\"2/1/11\") 42 >>> DAYS(DATE(2011, 12, 31), DATE(2011, 1, 1)) 364 >>> DAYS(\"2/1/11\", \"3/15/11\") -42","title":"DAYS"},{"location":"functions/#dtime","text":"DTIME (value, tz=None) # Returns the value converted to a python datetime object. The value may be a string , date (interpreted as midnight on that day), time (interpreted as a time-of-day today), or an existing datetime . The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. If the input is itself a datetime with the timezone set, it is returned unchanged (no changes to its timezone). >>> DTIME(datetime.date(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.date(2017, 1, 1), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('Europe/Paris')) >>> DTIME(datetime.datetime(2017, 1, 1)) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC'))) datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC')), 'Europe/Paris') datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC')) >>> DTIME(\"1/1/2008\") datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))","title":"DTIME"},{"location":"functions/#edate","text":"EDATE (start_date, months) # Returns the date that is the given number of months before or after start_date . Use EDATE to calculate maturity dates or due dates that fall on the same day of the month as the date of issue. >>> EDATE(DATE(2011, 1, 15), 1) datetime.date(2011, 2, 15) >>> EDATE(DATE(2011, 1, 15), -1) datetime.date(2010, 12, 15) >>> EDATE(DATE(2011, 1, 15), 2) datetime.date(2011, 3, 15) >>> EDATE(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 1) >>> EDATE(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 1)","title":"EDATE"},{"location":"functions/#eomonth","text":"EOMONTH (start_date, months) # Returns the date for the last day of the month that is the indicated number of months before or after start_date. Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. >>> EOMONTH(DATE(2011, 1, 1), 1) datetime.date(2011, 2, 28) >>> EOMONTH(DATE(2011, 1, 15), -3) datetime.date(2010, 10, 31) >>> EOMONTH(DATE(2012, 3, 1), 10) datetime.date(2013, 1, 31) >>> EOMONTH(DATE(2012, 5, 1), -2) datetime.date(2012, 3, 31)","title":"EOMONTH"},{"location":"functions/#hour","text":"HOUR (time) # Same as time.hour . >>> HOUR(XL_TO_DATE(0.75)) 18 >>> HOUR(\"7/18/2011 7:45\") 7 >>> HOUR(\"4/21/2012\") 0","title":"HOUR"},{"location":"functions/#isoweeknum","text":"ISOWEEKNUM (date) # Returns the ISO week number of the year for a given date. >>> ISOWEEKNUM(\"3/9/2012\") 10 >>> [ISOWEEKNUM(DATE(2000 + y, 1, 1)) for y in [0,1,2,3,4,5,6,7,8]] [52, 1, 1, 1, 1, 53, 52, 1, 1]","title":"ISOWEEKNUM"},{"location":"functions/#minute","text":"MINUTE (time) # Returns the minutes of datetime , as an integer from 0 to 59. Same as time.minute . >>> MINUTE(XL_TO_DATE(0.75)) 0 >>> MINUTE(\"7/18/2011 7:45\") 45 >>> MINUTE(\"12:59:00 PM\") 59 >>> MINUTE(datetime.time(12, 58, 59)) 58","title":"MINUTE"},{"location":"functions/#month","text":"MONTH (date) # Returns the month of a date represented, as an integer from from 1 (January) to 12 (December). Same as date.month . >>> MONTH(DATE(2011, 4, 15)) 4 >>> MONTH(\"5/31/2012\") 5 >>> MONTH(datetime.datetime(1900, 1, 1)) 1","title":"MONTH"},{"location":"functions/#now","text":"NOW (tz=None) # Returns the datetime object for the current time.","title":"NOW"},{"location":"functions/#second","text":"SECOND (time) # Returns the seconds of datetime , as an integer from 0 to 59. Same as time.second . >>> SECOND(XL_TO_DATE(0.75)) 0 >>> SECOND(\"7/18/2011 7:45:13\") 13 >>> SECOND(datetime.time(12, 58, 59)) 59","title":"SECOND"},{"location":"functions/#today","text":"TODAY (tz=None) # Returns the date object for the current date.","title":"TODAY"},{"location":"functions/#weekday","text":"WEEKDAY (date, return_type=1) # Returns the day of the week corresponding to a date. The day is given as an integer, ranging from 1 (Sunday) to 7 (Saturday), by default. Return_type determines the type of the returned value. 1 (default) - Returns 1 (Sunday) through 7 (Saturday). 2 - Returns 1 (Monday) through 7 (Sunday). 3 - Returns 0 (Monday) through 6 (Sunday). 11 - Returns 1 (Monday) through 7 (Sunday). 12 - Returns 1 (Tuesday) through 7 (Monday). 13 - Returns 1 (Wednesday) through 7 (Tuesday). 14 - Returns 1 (Thursday) through 7 (Wednesday). 15 - Returns 1 (Friday) through 7 (Thursday). 16 - Returns 1 (Saturday) through 7 (Friday). 17 - Returns 1 (Sunday) through 7 (Saturday). >>> WEEKDAY(DATE(2008, 2, 14)) 5 >>> WEEKDAY(DATE(2012, 3, 1)) 5 >>> WEEKDAY(DATE(2012, 3, 1), 1) 5 >>> WEEKDAY(DATE(2012, 3, 1), 2) 4 >>> WEEKDAY(\"3/1/2012\", 3) 3","title":"WEEKDAY"},{"location":"functions/#weeknum","text":"WEEKNUM (date, return_type=1) # Returns the week number of a specific date. For example, the week containing January 1 is the first week of the year, and is numbered week 1. Return_type determines which week is considered the first week of the year. 1 (default) - Week 1 is the first week starting Sunday that contains January 1. 2 - Week 1 is the first week starting Monday that contains January 1. 11 - Week 1 is the first week starting Monday that contains January 1. 12 - Week 1 is the first week starting Tuesday that contains January 1. 13 - Week 1 is the first week starting Wednesday that contains January 1. 14 - Week 1 is the first week starting Thursday that contains January 1. 15 - Week 1 is the first week starting Friday that contains January 1. 16 - Week 1 is the first week starting Saturday that contains January 1. 17 - Week 1 is the first week starting Sunday that contains January 1. 21 - ISO 8601 Approach: Week 1 is the first week starting Monday that contains January 4. Equivalently, it is the week that contains the first Thursday of the year. >>> WEEKNUM(DATE(2012, 3, 9)) 10 >>> WEEKNUM(DATE(2012, 3, 9), 2) 11 >>> WEEKNUM('1/1/1900') 1 >>> WEEKNUM('2/1/1900') 5","title":"WEEKNUM"},{"location":"functions/#xl_to_date","text":"XL_TO_DATE (value, tz=None) # Converts a provided Excel serial number representing a date into a datetime object. Value is interpreted as the number of days since December 30, 1899. (This corresponds to Google Sheets interpretation. Excel starts with Dec. 31, 1899 but wrongly considers 1900 to be a leap year. Excel for Mac should be configured to use 1900 date system, i.e. uncheck \u201cUse the 1904 date system\u201d option.) The returned datetime will have its timezone set to the tz argument, or the document\u2019s default timezone when tz is omitted or None. >>> XL_TO_DATE(41100.1875) datetime.datetime(2012, 7, 10, 4, 30, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(39448) datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York')) >>> XL_TO_DATE(40982.0625) datetime.datetime(2012, 3, 14, 1, 30, tzinfo=moment.tzinfo('America/New_York'))","title":"XL_TO_DATE"},{"location":"functions/#year","text":"YEAR (date) # Returns the year corresponding to a date as an integer. Same as date.year . >>> YEAR(DATE(2011, 4, 15)) 2011 >>> YEAR(\"5/31/2030\") 2030 >>> YEAR(datetime.datetime(1900, 1, 1)) 1900","title":"YEAR"},{"location":"functions/#yearfrac","text":"YEARFRAC (start_date, end_date, basis=0) # Calculates the fraction of the year represented by the number of whole days between two dates. Basis is the type of day count basis to use. 0 (default) - US (NASD) 30/360 1 - Actual/actual 2 - Actual/360 3 - Actual/365 4 - European 30/360 -1 - Actual/actual (Google Sheets variation) This function is useful for financial calculations. For compatibility with Excel, it defaults to using the NASD standard calendar. For use in non-financial settings, option -1 is likely the best choice. See https://en.wikipedia.org/wiki/360-day_calendar for explanation of the US 30/360 and European 30/360 methods. See http://www.dwheeler.com/yearfrac/ for analysis of Excel\u2019s particular implementation. Basis -1 is similar to 1 , but differs from Excel when dates span both leap and non-leap years. It matches the calculation in Google Sheets, counting the days in each year as a fraction of that year\u2019s length. Fraction of the year between 1/1/2012 and 7/30/12, omitting the Basis argument. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30)) '0.58055556' Fraction between same dates, using the Actual/Actual basis argument. Because 2012 is a Leap year, it has a 366 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 1) '0.57650273' Fraction between same dates, using the Actual/365 basis argument. Uses a 365 day basis. >>> \"%.8f\" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 3) '0.57808219'","title":"YEARFRAC"},{"location":"functions/#info","text":"","title":"Info"},{"location":"functions/#cell","text":"CELL (info_type, reference) # Returns the requested information about the specified cell. This is not implemented in Grist Note This function is not currently implemented in Grist.","title":"CELL"},{"location":"functions/#current_conversion","text":"CURRENT_CONVERSION (rec) # Internal function used by Grist during column type conversions. Not available for use in formulas.","title":"CURRENT_CONVERSION"},{"location":"functions/#isblank","text":"ISBLANK (value) # Returns whether a value refers to an empty cell. It isn\u2019t implemented in Grist. To check for an empty string, use value == \"\" . Note This function is not currently implemented in Grist.","title":"ISBLANK"},{"location":"functions/#isemail","text":"ISEMAIL (value) # Returns whether a value is a valid email address. Note that checking email validity is not an exact science. The technical standard considers many email addresses valid that are not used in practice, and would not be considered valid by most users. Instead, we follow Google Sheets implementation, with some differences, noted below. >>> ISEMAIL(\"Abc.123@example.com\") True >>> ISEMAIL(\"Bob_O-Reilly+tag@example.com\") True >>> ISEMAIL(\"John Doe\") False >>> ISEMAIL(\"john@aol...com\") False","title":"ISEMAIL"},{"location":"functions/#iserr","text":"ISERR (value) # Checks whether a value is an error. In other words, it returns true if using value directly would raise an exception. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. A more Pythonic approach to checking for errors is: try: ... value ... except Exception, err: ... do something about the error ... For example: >>> ISERR(\"Hello\") False","title":"ISERR"},{"location":"functions/#iserror","text":"ISERROR (value) # Checks whether a value is an error or an invalid value. It is similar to ISERR , but also returns true for an invalid value such as NaN or a text value in a Numeric column. NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation. >>> ISERROR(\"Hello\") False >>> ISERROR(AltText(\"fail\")) True >>> ISERROR(float('nan')) True","title":"ISERROR"},{"location":"functions/#islogical","text":"ISLOGICAL (value) # Checks whether a value is True or False . >>> ISLOGICAL(True) True >>> ISLOGICAL(False) True >>> ISLOGICAL(0) False >>> ISLOGICAL(None) False >>> ISLOGICAL(\"Test\") False","title":"ISLOGICAL"},{"location":"functions/#isna","text":"ISNA (value) # Checks whether a value is the error #N/A . >>> ISNA(float('nan')) True >>> ISNA(0.0) False >>> ISNA('text') False >>> ISNA(float('-inf')) False","title":"ISNA"},{"location":"functions/#isnontext","text":"ISNONTEXT (value) # Checks whether a value is non-textual. >>> ISNONTEXT(\"asdf\") False >>> ISNONTEXT(\"\") False >>> ISNONTEXT(AltText(\"text\")) False >>> ISNONTEXT(17.0) True >>> ISNONTEXT(None) True >>> ISNONTEXT(datetime.date(2011, 1, 1)) True","title":"ISNONTEXT"},{"location":"functions/#isnumber","text":"ISNUMBER (value) # Checks whether a value is a number. >>> ISNUMBER(17) True >>> ISNUMBER(-123.123423) True >>> ISNUMBER(False) True >>> ISNUMBER(float('nan')) True >>> ISNUMBER(float('inf')) True >>> ISNUMBER('17') False >>> ISNUMBER(None) False >>> ISNUMBER(datetime.date(2011, 1, 1)) False","title":"ISNUMBER"},{"location":"functions/#isref","text":"ISREF (value) # Checks whether a value is a table record. For example, if a column person is of type Reference to the People table, then ISREF($person) is True . Similarly, ISREF(People.lookupOne(name=$name)) is True . For any other type of value, ISREF() would evaluate to False . >>> ISREF(17) False >>> ISREF(\"Roger\") False","title":"ISREF"},{"location":"functions/#isreflist","text":"ISREFLIST (value) # Checks whether a value is a RecordSet , the type of values in Reference List columns. For example, if a column people is of type Reference List to the People table, then ISREFLIST($people) is True . Similarly, ISREFLIST(People.lookupRecords(name=$name)) is True . For any other type of value, ISREFLIST() would evaluate to False . >>> ISREFLIST(17) False >>> ISREFLIST(\"Roger\") False","title":"ISREFLIST"},{"location":"functions/#istext","text":"ISTEXT (value) # Checks whether a value is text. >>> ISTEXT(\"asdf\") True >>> ISTEXT(\"\") True >>> ISTEXT(AltText(\"text\")) True >>> ISTEXT(17.0) False >>> ISTEXT(None) False >>> ISTEXT(datetime.date(2011, 1, 1)) False","title":"ISTEXT"},{"location":"functions/#isurl","text":"ISURL (value) # Checks whether a value is a valid URL. It does not need to be fully qualified, or to include \u201chttp://\u201d and \u201cwww\u201d. It does not follow a standard, but attempts to work similarly to ISURL in Google Sheets, and to return True for text that is likely a URL. Valid protocols include ftp, http, https, gopher, mailto, news, telnet, and aim. >>> ISURL(\"http://www.getgrist.com\") True >>> ISURL(\"https://foo.com/test_(wikipedia)#cite-1\") True >>> ISURL(\"mailto://user@example.com\") True >>> ISURL(\"http:///a\") False","title":"ISURL"},{"location":"functions/#n","text":"N (value) # Returns the value converted to a number. True/False are converted to 1/0. A date is converted to Excel-style serial number of the date. Anything else is converted to 0. >>> N(7) 7 >>> N(7.1) 7.1 >>> N(\"Even\") 0 >>> N(\"7\") 0 >>> N(True) 1 >>> N(datetime.datetime(2011, 4, 17)) 40650.0","title":"N"},{"location":"functions/#na","text":"NA () # Returns the \u201cvalue not available\u201d error, #N/A . >>> math.isnan(NA()) True","title":"NA"},{"location":"functions/#peek","text":"PEEK (func) # Evaluates the given expression without creating dependencies or requiring that referenced values are up to date, using whatever value it finds in a cell. This is useful for preventing circular reference errors, particularly in trigger formulas. For example, if the formula for A depends on $B and the formula for B depends on $A , then normally this would raise a circular reference error because each value needs to be calculated before the other. But if A uses PEEK($B) then it will simply get the value already stored in $B without requiring that $B is first calculated to the latest value. Therefore A will be calculated first, and B can use $A without problems.","title":"PEEK"},{"location":"functions/#record_1","text":"RECORD (record_or_list, dates_as_iso=False, expand_refs=0) # Returns a Python dictionary with all fields in the given record. If a list of records is given, returns a list of corresponding Python dictionaries. If dates_as_iso is set, Date and DateTime values are converted to string using ISO 8601 format. If expand_refs is set to 1 or higher, Reference values are replaced with a RECORD representation of the referenced record, expanding the given number of levels. Error values present in cells of the record are replaced with None value, and a special key of \u201c error \u201d gets added containing the error messages for those cells. For example: {\"Ratio\": None, \"_error_\": {\"Ratio\": \"ZeroDivisionError: integer division or modulo by zero\"}} Note that care is needed to avoid circular references when using RECORD(), since it creates a dependency on every cell in the record. In case of RECORD(rec), the cell containing this call will be omitted from the resulting dictionary. For example: RECORD($Person) RECORD(rec) RECORD(People.lookupOne(First_Name=\"Alice\")) RECORD(People.lookupRecords(Department=\"HR\"))","title":"RECORD"},{"location":"functions/#request","text":"REQUEST (url, params=None, headers=None) # Note This function is not currently implemented in Grist.","title":"REQUEST"},{"location":"functions/#type","text":"TYPE (value) # Returns a number associated with the type of data passed into the function. This is not implemented in Grist. Use isinstance(value, type) or type(value) . Note This function is not currently implemented in Grist.","title":"TYPE"},{"location":"functions/#logical","text":"","title":"Logical"},{"location":"functions/#and","text":"AND (logical_expression, *logical_expressions) # Returns True if all of the arguments are logically true, and False if any are false. Same as all([value1, value2, ...]) . >>> AND(1) True >>> AND(0) False >>> AND(1, 1) True >>> AND(1,2,3,4) True >>> AND(1,2,3,4,0) False","title":"AND"},{"location":"functions/#false","text":"FALSE () # Returns the logical value False . You may also use the value False directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> FALSE() False","title":"FALSE"},{"location":"functions/#if","text":"IF (logical_expression, value_if_true, value_if_false) # Returns one value if a logical expression is True and another if it is False . The equivalent Python expression is: value_if_true if logical_expression else value_if_false Since Grist supports multi-line formulas, you may also use Python blocks such as: if logical_expression: return value_if_true else: return value_if_false NOTE: Grist follows Excel model by only evaluating one of the value expressions, by automatically wrapping the expressions to use lazy evaluation. This allows IF(False, 1/0, 1) to evaluate to 1 rather than raise an exception. >>> IF(12, \"Yes\", \"No\") 'Yes' >>> IF(None, \"Yes\", \"No\") 'No' >>> IF(True, 0.85, 0.0) 0.85 >>> IF(False, 0.85, 0.0) 0.0","title":"IF"},{"location":"functions/#iferror","text":"IFERROR (value, value_if_error=\u2019\u2018) # Returns the first argument if it is not an error value, otherwise returns the second argument if present, or a blank if the second argument is absent. NOTE: Grist handles values that raise an exception by wrapping them to use lazy evaluation. >>> IFERROR(float('nan'), \"**NAN**\") '**NAN**' >>> IFERROR(17.17, \"**NAN**\") 17.17 >>> IFERROR(\"Text\") 'Text' >>> IFERROR(AltText(\"hello\")) ''","title":"IFERROR"},{"location":"functions/#not","text":"NOT (logical_expression) # True . Same as not logical_expression . >>> NOT(123) False >>> NOT(0) True","title":"NOT"},{"location":"functions/#or","text":"OR (logical_expression, *logical_expressions) # Returns True if any of the arguments is logically true, and false if all of the arguments are false. Same as any([value1, value2, ...]) . >>> OR(1) True >>> OR(0) False >>> OR(1, 1) True >>> OR(0, 1) True >>> OR(0, 0) False >>> OR(0,False,0.0,\"\",None) False >>> OR(0,None,3,0) True","title":"OR"},{"location":"functions/#true","text":"TRUE () # Returns the logical value True . You may also use the value True directly. This function is provided primarily for compatibility with other spreadsheet programs. >>> TRUE() True","title":"TRUE"},{"location":"functions/#lookup","text":"","title":"Lookup"},{"location":"functions/#lookupone_1","text":"UserTable. lookupOne (Field_In_Lookup_Table=value, \u2026) # Returns a Record matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ). If multiple records match, returns one of them. If none match, returns the special empty record. For example: People.lookupOne(First_Name=\"Lewis\", Last_Name=\"Carroll\") People.lookupOne(Email=$Work_Email)","title":"lookupOne"},{"location":"functions/#lookuprecords_1","text":"UserTable. lookupRecords (Field_In_Lookup_Table=value, \u2026) # Returns a RecordSet matching the given field=value arguments. The value may be any expression, most commonly a field in the current row (e.g. $SomeField ) or a constant (e.g. a quoted string like \"Some Value\" ) (examples below). If sort_by=field is given, sort the results by that field. For example: People.lookupRecords(Email=$Work_Email) People.lookupRecords(First_Name=\"George\", Last_Name=\"Washington\") People.lookupRecords(Last_Name=\"Johnson\", sort_by=\"First_Name\") See RecordSet for useful properties offered by the returned object. See CONTAINS for an example utilizing UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value.","title":"lookupRecords"},{"location":"functions/#address","text":"ADDRESS (row, column, absolute_relative_mode, use_a1_notation, sheet) # Returns a cell reference as a string. Note This function is not currently implemented in Grist.","title":"ADDRESS"},{"location":"functions/#choose","text":"CHOOSE (index, choice1, choice2) # Returns an element from a list of choices based on index. Note This function is not currently implemented in Grist.","title":"CHOOSE"},{"location":"functions/#column","text":"COLUMN (cell_reference=None) # Returns the column number of a specified cell, with A=1 . Note This function is not currently implemented in Grist.","title":"COLUMN"},{"location":"functions/#columns","text":"COLUMNS (range) # Returns the number of columns in a specified array or range. Note This function is not currently implemented in Grist.","title":"COLUMNS"},{"location":"functions/#contains","text":"CONTAINS (value, match_empty=no_match_empty) # Use this marker with UserTable.lookupRecords to find records where a field of a list type (such as Choice List or Reference List ) contains the given value. For example: MoviesTable.lookupRecords(genre=CONTAINS(\"Drama\")) will return records in MoviesTable where the column genre is a list or other container such as [\"Comedy\", \"Drama\"] , i.e. \"Drama\" in $genre . Note that the column being looked up (e.g. genre ) must have values of a container type such as list, tuple, or set. In particular the values mustn\u2019t be strings, e.g. \"Comedy-Drama\" won\u2019t match even though \"Drama\" in \"Comedy-Drama\" is True in Python. It also won\u2019t match substrings within container elements, e.g. [\"Comedy-Drama\"] . You can optionally pass a second argument match_empty to indicate a value that should be matched against empty lists in the looked up column. For example, given this formula: MoviesTable.lookupRecords(genre=CONTAINS(g, match_empty='')) If g is '' (i.e. equal to match_empty ) then the column genre in the returned records will either be an empty list (or other container) or a list containing g as usual.","title":"CONTAINS"},{"location":"functions/#getpivotdata","text":"GETPIVOTDATA (value_name, any_pivot_table_cell, original_column_1, pivot_item_1=None, *args) # Extracts an aggregated value from a pivot table that corresponds to the specified row and column headings. Note This function is not currently implemented in Grist.","title":"GETPIVOTDATA"},{"location":"functions/#hlookup","text":"HLOOKUP (search_key, range, index, is_sorted) # Horizontal lookup. Searches across the first row of a range for a key and returns the value of a specified cell in the column found. Note This function is not currently implemented in Grist.","title":"HLOOKUP"},{"location":"functions/#hyperlink","text":"HYPERLINK (url, link_label) # Creates a hyperlink inside a cell. Note This function is not currently implemented in Grist.","title":"HYPERLINK"},{"location":"functions/#index","text":"INDEX (reference, row, column) # Returns the content of a cell, specified by row and column offset. Note This function is not currently implemented in Grist.","title":"INDEX"},{"location":"functions/#indirect","text":"INDIRECT (cell_reference_as_string) # Returns a cell reference specified by a string. Note This function is not currently implemented in Grist.","title":"INDIRECT"},{"location":"functions/#lookup_1","text":"LOOKUP (search_key, search_range_or_search_result_array, result_range=None) # Looks through a row or column for a key and returns the value of the cell in a result range located in the same position as the search row or column. Note This function is not currently implemented in Grist.","title":"LOOKUP"},{"location":"functions/#match","text":"MATCH (search_key, range, search_type) # Returns the relative position of an item in a range that matches a specified value. Note This function is not currently implemented in Grist.","title":"MATCH"},{"location":"functions/#offset","text":"OFFSET (cell_reference, offset_rows, offset_columns, height, width) # Returns a range reference shifted a specified number of rows and columns from a starting cell reference. Note This function is not currently implemented in Grist.","title":"OFFSET"},{"location":"functions/#row","text":"ROW (cell_reference) # Returns the row number of a specified cell. Note This function is not currently implemented in Grist.","title":"ROW"},{"location":"functions/#rows","text":"ROWS (range) # Returns the number of rows in a specified array or range. Note This function is not currently implemented in Grist.","title":"ROWS"},{"location":"functions/#self_hyperlink","text":"SELF_HYPERLINK (label=None, page=None, **kwargs) # Creates a link to the current document. All parameters are optional. The returned string is in URL format, optionally preceded by a label and a space (the format expected for Grist Text columns with the HyperLink option enabled). A numeric page number can be supplied, which will create a link to the specified page. To find the numeric page number you need, visit a page and examine its URL for a /p/NN part. Any number of arguments of the form LinkKey_NAME may be provided, to set user.LinkKey.NAME values that will be available in access rules. For example, if a rule allows users to view rows when user.LinkKey.Code == rec.Code , we might want to create links with SELF_HYPERLINK(LinkKey_Code=$Code) . >>> SELF_HYPERLINK() u'https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(label='doc') u'doc https://docs.getgrist.com/sbaltsirg/Example' >>> SELF_HYPERLINK(page=2) u'https://docs.getgrist.com/sbaltsirg/Example/p/2' >>> SELF_HYPERLINK(LinkKey_Code='X1234') u'https://docs.getgrist.com/sbaltsirg/Example?Code_=X1234' >>> SELF_HYPERLINK(label='order', page=3, LinkKey_Code='X1234', LinkKey_Name='Bi Ngo') u'order https://docs.getgrist.com/sbaltsirg/Example/p/3?Code_=X1234&Name_=Bi+Ngo' >>> SELF_HYPERLINK(Linky_Link='Link') Traceback (most recent call last): ... TypeError: unexpected keyword argument 'Linky_Link' (not of form LinkKey_NAME)","title":"SELF_HYPERLINK"},{"location":"functions/#vlookup","text":"VLOOKUP (table, **field_value_pairs) # Vertical lookup. Searches the given table for a record matching the given field=value arguments. If multiple records match, returns one of them. If none match, returns the special empty record. The returned object is a record whose fields are available using .field syntax. For example, VLOOKUP(Employees, EmployeeID=$EmpID).Salary . Note that VLOOKUP isn\u2019t commonly needed in Grist, since Reference columns are the best way to link data between tables, and allow simple efficient usage such as $Person.Age . VLOOKUP is exactly quivalent to table.lookupOne(**field_value_pairs) . See lookupOne . For example: VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\") VLOOKUP(People, First_Name=\"Lewis\", Last_Name=\"Carroll\").Age","title":"VLOOKUP"},{"location":"functions/#math","text":"","title":"Math"},{"location":"functions/#abs","text":"ABS (value) # Returns the absolute value of a number. >>> ABS(2) 2 >>> ABS(-2) 2 >>> ABS(-4) 4","title":"ABS"},{"location":"functions/#acos","text":"ACOS (value) # Returns the inverse cosine of a value, in radians. >>> round(ACOS(-0.5), 9) 2.094395102 >>> round(ACOS(-0.5)*180/PI(), 10) 120.0","title":"ACOS"},{"location":"functions/#acosh","text":"ACOSH (value) # Returns the inverse hyperbolic cosine of a number. >>> ACOSH(1) 0.0 >>> round(ACOSH(10), 7) 2.9932228","title":"ACOSH"},{"location":"functions/#arabic","text":"ARABIC (roman_numeral) # Computes the value of a Roman numeral. >>> ARABIC(\"LVII\") 57 >>> ARABIC('mcmxii') 1912","title":"ARABIC"},{"location":"functions/#asin","text":"ASIN (value) # Returns the inverse sine of a value, in radians. >>> round(ASIN(-0.5), 9) -0.523598776 >>> round(ASIN(-0.5)*180/PI(), 10) -30.0 >>> round(DEGREES(ASIN(-0.5)), 10) -30.0","title":"ASIN"},{"location":"functions/#asinh","text":"ASINH (value) # Returns the inverse hyperbolic sine of a number. >>> round(ASINH(-2.5), 9) -1.647231146 >>> round(ASINH(10), 9) 2.99822295","title":"ASINH"},{"location":"functions/#atan","text":"ATAN (value) # Returns the inverse tangent of a value, in radians. >>> round(ATAN(1), 9) 0.785398163 >>> ATAN(1)*180/PI() 45.0 >>> DEGREES(ATAN(1)) 45.0","title":"ATAN"},{"location":"functions/#atan2","text":"ATAN2 (x, y) # Returns the angle between the x-axis and a line segment from the origin (0,0) to specified coordinate pair ( x , y ), in radians. >>> round(ATAN2(1, 1), 9) 0.785398163 >>> round(ATAN2(-1, -1), 9) -2.35619449 >>> ATAN2(-1, -1)*180/PI() -135.0 >>> DEGREES(ATAN2(-1, -1)) -135.0 >>> round(ATAN2(1,2), 9) 1.107148718","title":"ATAN2"},{"location":"functions/#atanh","text":"ATANH (value) # Returns the inverse hyperbolic tangent of a number. >>> round(ATANH(0.76159416), 9) 1.00000001 >>> round(ATANH(-0.1), 9) -0.100335348","title":"ATANH"},{"location":"functions/#ceiling","text":"CEILING (value, factor=1) # Rounds a number up to the nearest multiple of factor, or the nearest integer if the factor is omitted or 1. >>> CEILING(2.5, 1) 3 >>> CEILING(-2.5, -2) -4 >>> CEILING(-2.5, 2) -2 >>> CEILING(1.5, 0.1) 1.5 >>> CEILING(0.234, 0.01) 0.24","title":"CEILING"},{"location":"functions/#combin","text":"COMBIN (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects. >>> COMBIN(8,2) 28 >>> COMBIN(4,2) 6 >>> COMBIN(10,7) 120","title":"COMBIN"},{"location":"functions/#cos","text":"COS (angle) # Returns the cosine of an angle provided in radians. >>> round(COS(1.047), 7) 0.5001711 >>> round(COS(60*PI()/180), 10) 0.5 >>> round(COS(RADIANS(60)), 10) 0.5","title":"COS"},{"location":"functions/#cosh","text":"COSH (value) # Returns the hyperbolic cosine of any real number. >>> round(COSH(4), 6) 27.308233 >>> round(COSH(EXP(1)), 7) 7.6101251","title":"COSH"},{"location":"functions/#degrees","text":"DEGREES (angle) # Converts an angle value in radians to degrees. >>> round(DEGREES(ACOS(-0.5)), 10) 120.0 >>> DEGREES(PI()) 180.0","title":"DEGREES"},{"location":"functions/#even","text":"EVEN (value) # Rounds a number up to the nearest even integer, rounding away from zero. >>> EVEN(1.5) 2 >>> EVEN(3) 4 >>> EVEN(2) 2 >>> EVEN(-1) -2","title":"EVEN"},{"location":"functions/#exp","text":"EXP (exponent) # Returns Euler\u2019s number, e (~2.718) raised to a power. >>> round(EXP(1), 8) 2.71828183 >>> round(EXP(2), 7) 7.3890561","title":"EXP"},{"location":"functions/#fact","text":"FACT (value) # Returns the factorial of a number. >>> FACT(5) 120 >>> FACT(1.9) 1 >>> FACT(0) 1 >>> FACT(1) 1 >>> FACT(-1) Traceback (most recent call last): ... ValueError: factorial() not defined for negative values","title":"FACT"},{"location":"functions/#factdouble","text":"FACTDOUBLE (value) # Returns the \u201cdouble factorial\u201d of a number. >>> FACTDOUBLE(6) 48 >>> FACTDOUBLE(7) 105 >>> FACTDOUBLE(3) 3 >>> FACTDOUBLE(4) 8","title":"FACTDOUBLE"},{"location":"functions/#floor","text":"FLOOR (value, factor=1) # Rounds a number down to the nearest integer multiple of specified significance. >>> FLOOR(3.7,2) 2 >>> FLOOR(-2.5,-2) -2 >>> FLOOR(2.5,-2) Traceback (most recent call last): ... ValueError: factor argument invalid >>> FLOOR(1.58,0.1) 1.5 >>> FLOOR(0.234,0.01) 0.23","title":"FLOOR"},{"location":"functions/#gcd","text":"GCD (value1, *more_values) # Returns the greatest common divisor of one or more integers. >>> GCD(5, 2) 1 >>> GCD(24, 36) 12 >>> GCD(7, 1) 1 >>> GCD(5, 0) 5 >>> GCD(0, 5) 5 >>> GCD(5) 5 >>> GCD(14, 42, 21) 7","title":"GCD"},{"location":"functions/#int","text":"INT (value) # Rounds a number down to the nearest integer that is less than or equal to it. >>> INT(8.9) 8 >>> INT(-8.9) -9 >>> 19.5-INT(19.5) 0.5","title":"INT"},{"location":"functions/#lcm","text":"LCM (value1, *more_values) # Returns the least common multiple of one or more integers. >>> LCM(5, 2) 10 >>> LCM(24, 36) 72 >>> LCM(0, 5) 0 >>> LCM(5) 5 >>> LCM(10, 100) 100 >>> LCM(12, 18) 36 >>> LCM(12, 18, 24) 72","title":"LCM"},{"location":"functions/#ln","text":"LN (value) # Returns the the logarithm of a number, base e (Euler\u2019s number). >>> round(LN(86), 7) 4.4543473 >>> round(LN(2.7182818), 7) 1.0 >>> round(LN(EXP(3)), 10) 3.0","title":"LN"},{"location":"functions/#log","text":"LOG (value, base=10) # Returns the the logarithm of a number given a base. >>> LOG(10) 1.0 >>> LOG(8, 2) 3.0 >>> round(LOG(86, 2.7182818), 7) 4.4543473","title":"LOG"},{"location":"functions/#log10","text":"LOG10 (value) # Returns the the logarithm of a number, base 10. >>> round(LOG10(86), 9) 1.934498451 >>> LOG10(10) 1.0 >>> LOG10(100000) 5.0 >>> LOG10(10**5) 5.0","title":"LOG10"},{"location":"functions/#mod","text":"MOD (dividend, divisor) # Returns the result of the modulo operator, the remainder after a division operation. >>> MOD(3, 2) 1 >>> MOD(-3, 2) 1 >>> MOD(3, -2) -1 >>> MOD(-3, -2) -1","title":"MOD"},{"location":"functions/#mround","text":"MROUND (value, factor) # Rounds one number to the nearest integer multiple of another. >>> MROUND(10, 3) 9 >>> MROUND(-10, -3) -9 >>> round(MROUND(1.3, 0.2), 10) 1.4 >>> MROUND(5, -2) Traceback (most recent call last): ... ValueError: factor argument invalid","title":"MROUND"},{"location":"functions/#multinomial","text":"MULTINOMIAL (value1, *more_values) # Returns the factorial of the sum of values divided by the product of the values\u2019 factorials. >>> MULTINOMIAL(2, 3, 4) 1260 >>> MULTINOMIAL(3) 1 >>> MULTINOMIAL(1,2,3) 60 >>> MULTINOMIAL(0,2,4,6) 13860","title":"MULTINOMIAL"},{"location":"functions/#odd","text":"ODD (value) # Rounds a number up to the nearest odd integer. >>> ODD(1.5) 3 >>> ODD(3) 3 >>> ODD(2) 3 >>> ODD(-1) -1 >>> ODD(-2) -3","title":"ODD"},{"location":"functions/#pi","text":"PI () # Returns the value of Pi to 14 decimal places. >>> round(PI(), 9) 3.141592654 >>> round(PI()/2, 9) 1.570796327 >>> round(PI()*9, 8) 28.27433388","title":"PI"},{"location":"functions/#power","text":"POWER (base, exponent) # Returns a number raised to a power. >>> POWER(5,2) 25.0 >>> round(POWER(98.6,3.2), 3) 2401077.222 >>> round(POWER(4,5.0/4), 9) 5.656854249","title":"POWER"},{"location":"functions/#product","text":"PRODUCT (factor1, *more_factors) # Returns the result of multiplying a series of numbers together. Each argument may be a number or an array. >>> PRODUCT([5,15,30]) 2250 >>> PRODUCT([5,15,30], 2) 4500 >>> PRODUCT(5,15,[30],[2]) 4500","title":"PRODUCT"},{"location":"functions/#quotient","text":"QUOTIENT (dividend, divisor) # Returns one number divided by another, without the remainder. >>> QUOTIENT(5, 2) 2 >>> QUOTIENT(4.5, 3.1) 1 >>> QUOTIENT(-10, 3) -3","title":"QUOTIENT"},{"location":"functions/#radians","text":"RADIANS (angle) # Converts an angle value in degrees to radians. >>> round(RADIANS(270), 6) 4.712389","title":"RADIANS"},{"location":"functions/#rand","text":"RAND () # Returns a random number between 0 inclusive and 1 exclusive.","title":"RAND"},{"location":"functions/#randbetween","text":"RANDBETWEEN (low, high) # Returns a uniformly random integer between two values, inclusive.","title":"RANDBETWEEN"},{"location":"functions/#roman","text":"ROMAN (number, form_unused=None) # Formats a number in Roman numerals. The second argument is ignored in this implementation. >>> ROMAN(499,0) 'CDXCIX' >>> ROMAN(499.2,0) 'CDXCIX' >>> ROMAN(57) 'LVII' >>> ROMAN(1912) 'MCMXII'","title":"ROMAN"},{"location":"functions/#round","text":"ROUND (value, places=0) # Rounds a number to a certain number of decimal places, by default to the nearest whole number if the number of places is not given. Rounds away from zero (\u2018up\u2019 for positive numbers) in the case of a tie, i.e. when the last digit is 5. >>> ROUND(1.4) 1.0 >>> ROUND(1.5) 2.0 >>> ROUND(2.5) 3.0 >>> ROUND(-2.5) -3.0 >>> ROUND(2.15, 1) 2.2 >>> ROUND(-1.475, 2) -1.48 >>> ROUND(21.5, -1) 20.0 >>> ROUND(626.3,-3) 1000.0 >>> ROUND(1.98,-1) 0.0 >>> ROUND(-50.55,-2) -100.0 >>> ROUND(0) 0.0","title":"ROUND"},{"location":"functions/#rounddown","text":"ROUNDDOWN (value, places=0) # Rounds a number to a certain number of decimal places, always rounding down towards zero. >>> ROUNDDOWN(3.2, 0) 3 >>> ROUNDDOWN(76.9,0) 76 >>> ROUNDDOWN(3.14159, 3) 3.141 >>> ROUNDDOWN(-3.14159, 1) -3.1 >>> ROUNDDOWN(31415.92654, -2) 31400","title":"ROUNDDOWN"},{"location":"functions/#roundup","text":"ROUNDUP (value, places=0) # Rounds a number to a certain number of decimal places, always rounding up away from zero. >>> ROUNDUP(3.2,0) 4 >>> ROUNDUP(76.9,0) 77 >>> ROUNDUP(3.14159, 3) 3.142 >>> ROUNDUP(-3.14159, 1) -3.2 >>> ROUNDUP(31415.92654, -2) 31500","title":"ROUNDUP"},{"location":"functions/#seriessum","text":"SERIESSUM (x, n, m, a) # Given parameters x, n, m, and a, returns the power series sum a_1 x^n + a_2 x^(n+m) + \u2026 + a_i*x^(n+(i-1)m), where i is the number of entries in range a . >>> SERIESSUM(1,0,1,1) 1 >>> SERIESSUM(2,1,0,[1,2,3]) 12 >>> SERIESSUM(-3,1,1,[2,4,6]) -132 >>> round(SERIESSUM(PI()/4,0,2,[1,-1./FACT(2),1./FACT(4),-1./FACT(6)]), 6) 0.707103","title":"SERIESSUM"},{"location":"functions/#sign","text":"SIGN (value) # Given an input number, returns -1 if it is negative, 1 if positive, and 0 if it is zero. >>> SIGN(10) 1 >>> SIGN(4.0-4.0) 0 >>> SIGN(-0.00001) -1","title":"SIGN"},{"location":"functions/#sin","text":"SIN (angle) # Returns the sine of an angle provided in radians. >>> round(SIN(PI()), 10) 0.0 >>> SIN(PI()/2) 1.0 >>> round(SIN(30*PI()/180), 10) 0.5 >>> round(SIN(RADIANS(30)), 10) 0.5","title":"SIN"},{"location":"functions/#sinh","text":"SINH (value) # Returns the hyperbolic sine of any real number. >>> round(2.868*SINH(0.0342*1.03), 7) 0.1010491","title":"SINH"},{"location":"functions/#sqrt","text":"SQRT (value) # Returns the positive square root of a positive number. >>> SQRT(16) 4.0 >>> SQRT(-16) Traceback (most recent call last): ... ValueError: math domain error >>> SQRT(ABS(-16)) 4.0","title":"SQRT"},{"location":"functions/#sqrtpi","text":"SQRTPI (value) # Returns the positive square root of the product of Pi and the given positive number. >>> round(SQRTPI(1), 6) 1.772454 >>> round(SQRTPI(2), 6) 2.506628","title":"SQRTPI"},{"location":"functions/#subtotal","text":"SUBTOTAL (function_code, range1, range2) # Returns a subtotal for a vertical range of cells using a specified aggregation function. Note This function is not currently implemented in Grist.","title":"SUBTOTAL"},{"location":"functions/#sum","text":"SUM (value1, *more_values) # Returns the sum of a series of numbers. Each argument may be a number or an array. Non-numeric values are ignored. >>> SUM([5,15,30]) 50 >>> SUM([5.,15,30], 2) 52.0 >>> SUM(5,15,[30],[2]) 52","title":"SUM"},{"location":"functions/#sumif","text":"SUMIF (records, criterion, sum_range) # Returns a conditional sum across a range. Note This function is not currently implemented in Grist.","title":"SUMIF"},{"location":"functions/#sumifs","text":"SUMIFS (sum_range, criteria_range1, criterion1, *args) # Returns the sum of a range depending on multiple criteria. Note This function is not currently implemented in Grist.","title":"SUMIFS"},{"location":"functions/#sumproduct","text":"SUMPRODUCT (array1, *more_arrays) # Multiplies corresponding components in two equally-sized arrays, and returns the sum of those products. >>> SUMPRODUCT([3,8,1,4,6,9], [2,6,5,7,7,3]) 156 >>> SUMPRODUCT([], [], []) 0 >>> SUMPRODUCT([-0.25], [-2], [-3]) -1.5 >>> SUMPRODUCT([-0.25, -0.25], [-2, -2], [-3, -3]) -3.0","title":"SUMPRODUCT"},{"location":"functions/#sumsq","text":"SUMSQ (value1, value2) # Returns the sum of the squares of a series of numbers and/or cells. Note This function is not currently implemented in Grist.","title":"SUMSQ"},{"location":"functions/#tan","text":"TAN (angle) # Returns the tangent of an angle provided in radians. >>> round(TAN(0.785), 8) 0.99920399 >>> round(TAN(45*PI()/180), 10) 1.0 >>> round(TAN(RADIANS(45)), 10) 1.0","title":"TAN"},{"location":"functions/#tanh","text":"TANH (value) # Returns the hyperbolic tangent of any real number. >>> round(TANH(-2), 6) -0.964028 >>> TANH(0) 0.0 >>> round(TANH(0.5), 6) 0.462117","title":"TANH"},{"location":"functions/#trunc","text":"TRUNC (value, places=0) # Truncates a number to a certain number of significant digits by omitting less significant digits. >>> TRUNC(8.9) 8 >>> TRUNC(-8.9) -8 >>> TRUNC(0.45) 0","title":"TRUNC"},{"location":"functions/#uuid","text":"UUID () # Generate a random UUID-formatted string identifier. Since UUID() produces a different value each time it\u2019s called, it is best to use it in trigger formula for new records. This would only calculate UUID() once and freeze the calculated value. By contrast, a regular formula may get recalculated any time the document is reloaded, producing a different value for UUID() each time.","title":"UUID"},{"location":"functions/#schedule","text":"","title":"Schedule"},{"location":"functions/#schedule_1","text":"SCHEDULE (schedule, start=None, count=10, end=None) # Returns the list of datetime objects generated according to the schedule string. Starts at start , which defaults to NOW(). Generates at most count results (10 by default). If end is given, stops there. The schedule has the format \u201cINTERVAL: SLOTS, \u2026\u201d. For example: annual: Jan-15, Apr-15, Jul-15 -- Three times a year on given dates at midnight. annual: 1/15, 4/15, 7/15 -- Same as above. monthly: /1 2pm, /15 2pm -- The 1st and the 15th of each month, at 2pm. 3-months: /10, +1m /20 -- Every 3 months on the 10th of month 1, 20th of month 2. weekly: Mo 9am, Tu 9am, Fr 2pm -- Three times a week at specified times. 2-weeks: Mo, +1w Tu -- Every 2 weeks on Monday of week 1, Tuesday of week 2. daily: 07:30, 21:00 -- Twice a day at specified times. 2-day: 12am, 4pm, +1d 8am -- Three times every two days, evenly spaced. hourly: :15, :45 -- 15 minutes before and after each hour. 4-hour: :00, 1:20, 2:40 -- Three times every 4 hours, evenly spaced. 10-minute: +0s -- Every 10 minutes on the minute. INTERVAL must be either of the form N-unit where N is a number and unit is one of year , month , week , day , hour ; or one of the aliases: annual , monthly , weekly , daily , hourly , which mean 1-year , 1-month , etc. SLOTS support the following units: `Jan-15` or `1/15` -- Month and day of the month; available when INTERVAL is year-based. `/15` -- Day of the month, available when INTERVAL is month-based. `Mon`, `Mo`, `Friday` -- Day of the week (or abbreviation), when INTERVAL is week-based. 10am, 1:30pm, 15:45 -- Time of day, available for day-based or longer intervals. :45, :00 -- Minutes of the hour, available when INTERVAL is hour-based. +1d, +15d -- How many days to add to start of INTERVAL. +1w -- How many weeks to add to start of INTERVAL. +1m -- How many months to add to start of INTERVAL. The SLOTS are always relative to the INTERVAL rather than to start . Week-based intervals start on Sunday. E.g. weekly: +1d, +4d is the same as weekly: Mon, Thu , and generates times on Mondays and Thursdays regardless of start . The first generated time is determined by the unit of the INTERVAL without regard to the multiple. E.g. both \u201c2-week: Mon\u201d and \u201c3-week: Mon\u201d start on the first Monday after start , and then generate either every second or every third Monday after that. Similarly, 24-hour: :00 starts with the first top-of-the-hour after start (not with midnight), and then repeats every 24 hours. To start with the midnight after start , use daily: 0:00 . For interval units of a day or longer, if time-of-day is not specified, it defaults to midnight. The time zone of start determines the time zone of the generated times. >>> def show(dates): return [d.strftime(\"%Y-%m-%d %H:%M\") for d in dates] >>> start = datetime(2018, 9, 4, 14, 0); # 2pm on Tue, Sep 4 2018. >>> show(SCHEDULE('annual: Jan-15, Apr-15, Jul-15, Oct-15', start=start, count=4)) ['2018-10-15 00:00', '2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00'] >>> show(SCHEDULE('annual: 1/15, 4/15, 7/15', start=start, count=4)) ['2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00', '2020-01-15 00:00'] >>> show(SCHEDULE('monthly: /1 2pm, /15 5pm', start=start, count=4)) ['2018-09-15 17:00', '2018-10-01 14:00', '2018-10-15 17:00', '2018-11-01 14:00'] >>> show(SCHEDULE('3-months: /10, +1m /20', start=start, count=4)) ['2018-09-10 00:00', '2018-10-20 00:00', '2018-12-10 00:00', '2019-01-20 00:00'] >>> show(SCHEDULE('weekly: Mo 9am, Tu 9am, Fr 2pm', start=start, count=4)) ['2018-09-07 14:00', '2018-09-10 09:00', '2018-09-11 09:00', '2018-09-14 14:00'] >>> show(SCHEDULE('2-weeks: Mo, +1w Tu', start=start, count=4)) ['2018-09-11 00:00', '2018-09-17 00:00', '2018-09-25 00:00', '2018-10-01 00:00'] >>> show(SCHEDULE('daily: 07:30, 21:00', start=start, count=4)) ['2018-09-04 21:00', '2018-09-05 07:30', '2018-09-05 21:00', '2018-09-06 07:30'] >>> show(SCHEDULE('2-day: 12am, 4pm, +1d 8am', start=start, count=4)) ['2018-09-04 16:00', '2018-09-05 08:00', '2018-09-06 00:00', '2018-09-06 16:00'] >>> show(SCHEDULE('hourly: :15, :45', start=start, count=4)) ['2018-09-04 14:15', '2018-09-04 14:45', '2018-09-04 15:15', '2018-09-04 15:45'] >>> show(SCHEDULE('4-hour: :00, +1H :20, +2H :40', start=start, count=4)) ['2018-09-04 14:00', '2018-09-04 15:20', '2018-09-04 16:40', '2018-09-04 18:00']","title":"SCHEDULE"},{"location":"functions/#stats","text":"","title":"Stats"},{"location":"functions/#avedev","text":"AVEDEV (value1, value2) # Calculates the average of the magnitudes of deviations of data from a dataset\u2019s mean. Note This function is not currently implemented in Grist.","title":"AVEDEV"},{"location":"functions/#average","text":"AVERAGE (value, *more_values) # Returns the numerical average value in a dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. >>> AVERAGE([2, -1.0, 11]) 4.0 >>> AVERAGE([2, -1, 11, \"Hello\"]) 4.0 >>> AVERAGE([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11]) 4.0 >>> AVERAGE(False, True) Traceback (most recent call last): ... ZeroDivisionError: float division by zero","title":"AVERAGE"},{"location":"functions/#averagea","text":"AVERAGEA (value, *more_values) # Returns the numerical average value in a dataset, counting non-numerical values as 0. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. >>> AVERAGEA([2, -1.0, 11]) 4.0 >>> AVERAGEA([2, -1, 11, \"Hello\"]) 3.0 >>> AVERAGEA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 1.5 >>> AVERAGEA(False, True) 0.5","title":"AVERAGEA"},{"location":"functions/#averageif","text":"AVERAGEIF (criteria_range, criterion, average_range=None) # Returns the average of a range depending on criteria. Note This function is not currently implemented in Grist.","title":"AVERAGEIF"},{"location":"functions/#averageifs","text":"AVERAGEIFS (average_range, criteria_range1, criterion1, *args) # Returns the average of a range depending on multiple criteria. Note This function is not currently implemented in Grist.","title":"AVERAGEIFS"},{"location":"functions/#average_weighted","text":"AVERAGE_WEIGHTED (pairs) # Given a list of (value, weight) pairs, finds the average of the values weighted by the corresponding weights. Ignores any pairs with a non-numerical value or weight. If you have two lists, of values and weights, use the Python built-in zip() function to create a list of pairs. >>> AVERAGE_WEIGHTED(((95, .25), (90, .1), (\"X\", .5), (85, .15), (88, .2), (82, .3), (70, None))) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, \"X\", 85, 88, 82, 70], [25, 10, 50, 15, 20, 30, None])) 87.7 >>> AVERAGE_WEIGHTED(zip([95, 90, False, 85, 88, 82, 70], [.25, .1, .5, .15, .2, .3, True])) 87.7","title":"AVERAGE_WEIGHTED"},{"location":"functions/#binomdist","text":"BINOMDIST (num_successes, num_trials, prob_success, cumulative) # Calculates the probability of drawing a certain number of successes (or a maximum number of successes) in a certain number of tries given a population of a certain size containing a certain number of successes, with replacement of draws. Note This function is not currently implemented in Grist.","title":"BINOMDIST"},{"location":"functions/#confidence","text":"CONFIDENCE (alpha, standard_deviation, pop_size) # Calculates the width of half the confidence interval for a normal distribution. Note This function is not currently implemented in Grist.","title":"CONFIDENCE"},{"location":"functions/#correl","text":"CORREL (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist.","title":"CORREL"},{"location":"functions/#count","text":"COUNT (value, *more_values) # Returns the count of numerical and date/datetime values in a dataset, ignoring other types of values. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. >>> COUNT([2, -1.0, 11]) 3 >>> COUNT([2, -1, 11, \"Hello\"]) 3 >>> COUNT([DATE(2000, 1, 1), DATE(2000, 1, 2), DATE(2000, 1, 3), \"Hello\"]) 3 >>> COUNT([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 4 >>> COUNT(False, True) 0","title":"COUNT"},{"location":"functions/#counta","text":"COUNTA (value, *more_values) # Returns the count of all values in a dataset, including non-numerical values. Each argument may be a value or an array. >>> COUNTA([2, -1.0, 11]) 3 >>> COUNTA([2, -1, 11, \"Hello\"]) 4 >>> COUNTA([2, -1, \"Hello\", DATE(2015,1,1)], True, [False, \"123\", \"\", 11.5]) 9 >>> COUNTA(False, True) 2","title":"COUNTA"},{"location":"functions/#covar","text":"COVAR (data_y, data_x) # Calculates the covariance of a dataset. Note This function is not currently implemented in Grist.","title":"COVAR"},{"location":"functions/#critbinom","text":"CRITBINOM (num_trials, prob_success, target_prob) # Calculates the smallest value for which the cumulative binomial distribution is greater than or equal to a specified criteria. Note This function is not currently implemented in Grist.","title":"CRITBINOM"},{"location":"functions/#devsq","text":"DEVSQ (value1, value2) # Calculates the sum of squares of deviations based on a sample. Note This function is not currently implemented in Grist.","title":"DEVSQ"},{"location":"functions/#expondist","text":"EXPONDIST (x, lambda_, cumulative) # Returns the value of the exponential distribution function with a specified lambda at a specified value. Note This function is not currently implemented in Grist.","title":"EXPONDIST"},{"location":"functions/#fdist","text":"FDIST (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist.","title":"FDIST"},{"location":"functions/#fisher","text":"FISHER (value) # Returns the Fisher transformation of a specified value. Note This function is not currently implemented in Grist.","title":"FISHER"},{"location":"functions/#fisherinv","text":"FISHERINV (value) # Returns the inverse Fisher transformation of a specified value. Note This function is not currently implemented in Grist.","title":"FISHERINV"},{"location":"functions/#forecast","text":"FORECAST (x, data_y, data_x) # Calculates the expected y-value for a specified x based on a linear regression of a dataset. Note This function is not currently implemented in Grist.","title":"FORECAST"},{"location":"functions/#f_dist","text":"F_DIST (x, degrees_freedom1, degrees_freedom2, cumulative) # Calculates the left-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist.","title":"F_DIST"},{"location":"functions/#f_dist_rt","text":"F_DIST_RT (x, degrees_freedom1, degrees_freedom2) # Calculates the right-tailed F probability distribution (degree of diversity) for two data sets with given input x. Alternately called Fisher-Snedecor distribution or Snedecor\u2019s F distribution. Note This function is not currently implemented in Grist.","title":"F_DIST_RT"},{"location":"functions/#geomean","text":"GEOMEAN (value1, value2) # Calculates the geometric mean of a dataset. Note This function is not currently implemented in Grist.","title":"GEOMEAN"},{"location":"functions/#harmean","text":"HARMEAN (value1, value2) # Calculates the harmonic mean of a dataset. Note This function is not currently implemented in Grist.","title":"HARMEAN"},{"location":"functions/#hypgeomdist","text":"HYPGEOMDIST (num_successes, num_draws, successes_in_pop, pop_size) # Calculates the probability of drawing a certain number of successes in a certain number of tries given a population of a certain size containing a certain number of successes, without replacement of draws. Note This function is not currently implemented in Grist.","title":"HYPGEOMDIST"},{"location":"functions/#intercept","text":"INTERCEPT (data_y, data_x) # Calculates the y-value at which the line resulting from linear regression of a dataset will intersect the y-axis (x=0). Note This function is not currently implemented in Grist.","title":"INTERCEPT"},{"location":"functions/#kurt","text":"KURT (value1, value2) # Calculates the kurtosis of a dataset, which describes the shape, and in particular the \u201cpeakedness\u201d of that dataset. Note This function is not currently implemented in Grist.","title":"KURT"},{"location":"functions/#large","text":"LARGE (data, n) # Returns the nth largest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist.","title":"LARGE"},{"location":"functions/#loginv","text":"LOGINV (x, mean, standard_deviation) # Returns the value of the inverse log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist.","title":"LOGINV"},{"location":"functions/#lognormdist","text":"LOGNORMDIST (x, mean, standard_deviation) # Returns the value of the log-normal cumulative distribution with given mean and standard deviation at a specified value. Note This function is not currently implemented in Grist.","title":"LOGNORMDIST"},{"location":"functions/#max","text":"MAX (value, *more_values) # Returns the maximum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MAX([2, -1.5, 11.5]) 11.5 >>> MAX([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAX(True, -123) -123 >>> MAX(\"123\", -123) -123 >>> MAX(\"Hello\", \"123\", True, False) 0 >>> MAX(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 2) >>> MAX(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56) >>> MAX(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 2)","title":"MAX"},{"location":"functions/#maxa","text":"MAXA (value, *more_values) # Returns the maximum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MAXA([2, -1.5, 11.5]) 11.5 >>> MAXA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) 11.5 >>> MAXA(True, -123) 1 >>> MAXA(\"123\", -123) 0 >>> MAXA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0","title":"MAXA"},{"location":"functions/#median","text":"MEDIAN (value, *more_values) # Returns the median value in a numeric dataset, ignoring non-numerical values. Each argument may be a value or an array. Values that are not numbers, including logical and blank values, and text representations of numbers, are ignored. Produces an error if the arguments contain no numbers. The median is the middle number when all values are sorted. So half of the values in the dataset are less than the median, and half of the values are greater. If there is an even number of values in the dataset, returns the average of the two numbers in the middle. >>> MEDIAN(1, 2, 3, 4, 5) 3 >>> MEDIAN(3, 5, 1, 4, 2) 3 >>> MEDIAN(range(10)) 4.5 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1), 12.3) 12.3 >>> MEDIAN(\"Hello\", \"123\", DATE(2015, 1, 1)) Traceback (most recent call last): ... ValueError: MEDIAN requires at least one number","title":"MEDIAN"},{"location":"functions/#min","text":"MIN (value, *more_values) # Returns the minimum value in a dataset, ignoring values other than numbers and dates/datetimes. Each argument may be a value or an array. Values that are not numbers or dates, including logical and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments contain no numbers or dates. >>> MIN([2, -1.5, 11.5]) -1.5 >>> MIN([2, -1.5, \"Hello\"], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MIN(True, 123) 123 >>> MIN(\"-123\", 123) 123 >>> MIN(\"Hello\", \"123\", True, False) 0 >>> MIN(DATE(2015, 1, 1), DATE(2015, 1, 2)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 1), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.date(2015, 1, 1) >>> MIN(DATE(2015, 1, 2), datetime.datetime(2015, 1, 1, 12, 34, 56)) datetime.datetime(2015, 1, 1, 12, 34, 56)","title":"MIN"},{"location":"functions/#mina","text":"MINA (value, *more_values) # Returns the minimum numeric value in a dataset. Each argument may be a value of an array. Values that are not numbers, including dates and text representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and False as 0. Returns 0 if the arguments contain no numbers. >>> MINA([2, -1.5, 11.5]) -1.5 >>> MINA([2, -1.5, \"Hello\", DATE(2015, 1, 1)], True, [False, \"123\", \"\", 11.5]) -1.5 >>> MINA(True, 123) 1 >>> MINA(\"-123\", 123) 0 >>> MINA(\"Hello\", \"123\", DATE(2015, 1, 1)) 0","title":"MINA"},{"location":"functions/#mode","text":"MODE (value1, value2) # Returns the most commonly occurring value in a dataset. Note This function is not currently implemented in Grist.","title":"MODE"},{"location":"functions/#negbinomdist","text":"NEGBINOMDIST (num_failures, num_successes, prob_success) # Calculates the probability of drawing a certain number of failures before a certain number of successes given a probability of success in independent trials. Note This function is not currently implemented in Grist.","title":"NEGBINOMDIST"},{"location":"functions/#normdist","text":"NORMDIST (x, mean, standard_deviation, cumulative) # Returns the value of the normal distribution function (or normal cumulative distribution function) for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist.","title":"NORMDIST"},{"location":"functions/#norminv","text":"NORMINV (x, mean, standard_deviation) # Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation. Note This function is not currently implemented in Grist.","title":"NORMINV"},{"location":"functions/#normsdist","text":"NORMSDIST (x) # Returns the value of the standard normal cumulative distribution function for a specified value. Note This function is not currently implemented in Grist.","title":"NORMSDIST"},{"location":"functions/#normsinv","text":"NORMSINV (x) # Returns the value of the inverse standard normal distribution function for a specified value. Note This function is not currently implemented in Grist.","title":"NORMSINV"},{"location":"functions/#pearson","text":"PEARSON (data_y, data_x) # Calculates r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist.","title":"PEARSON"},{"location":"functions/#percentile","text":"PERCENTILE (data, percentile) # Returns the value at a given percentile of a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTILE"},{"location":"functions/#percentrank","text":"PERCENTRANK (data, value, significant_digits=None) # Returns the percentage rank (percentile) of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTRANK"},{"location":"functions/#percentrank_exc","text":"PERCENTRANK_EXC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 exclusive of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTRANK_EXC"},{"location":"functions/#percentrank_inc","text":"PERCENTRANK_INC (data, value, significant_digits=None) # Returns the percentage rank (percentile) from 0 to 1 inclusive of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"PERCENTRANK_INC"},{"location":"functions/#permut","text":"PERMUT (n, k) # Returns the number of ways to choose some number of objects from a pool of a given size of objects, considering order. Note This function is not currently implemented in Grist.","title":"PERMUT"},{"location":"functions/#poisson","text":"POISSON (x, mean, cumulative) # Returns the value of the Poisson distribution function (or Poisson cumulative distribution function) for a specified value and mean. Note This function is not currently implemented in Grist.","title":"POISSON"},{"location":"functions/#prob","text":"PROB (data, probabilities, low_limit, high_limit=None) # Given a set of values and corresponding probabilities, calculates the probability that a value chosen at random falls between two limits. Note This function is not currently implemented in Grist.","title":"PROB"},{"location":"functions/#quartile","text":"QUARTILE (data, quartile_number) # Returns a value nearest to a specified quartile of a dataset. Note This function is not currently implemented in Grist.","title":"QUARTILE"},{"location":"functions/#rank","text":"RANK (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. Note This function is not currently implemented in Grist.","title":"RANK"},{"location":"functions/#rank_avg","text":"RANK_AVG (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the average rank of the entries will be returned. Note This function is not currently implemented in Grist.","title":"RANK_AVG"},{"location":"functions/#rank_eq","text":"RANK_EQ (value, data, is_ascending=None) # Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the top rank of the entries will be returned. Note This function is not currently implemented in Grist.","title":"RANK_EQ"},{"location":"functions/#rsq","text":"RSQ (data_y, data_x) # Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset. Note This function is not currently implemented in Grist.","title":"RSQ"},{"location":"functions/#skew","text":"SKEW (value1, value2) # Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean. Note This function is not currently implemented in Grist.","title":"SKEW"},{"location":"functions/#slope","text":"SLOPE (data_y, data_x) # Calculates the slope of the line resulting from linear regression of a dataset. Note This function is not currently implemented in Grist.","title":"SLOPE"},{"location":"functions/#small","text":"SMALL (data, n) # Returns the nth smallest element from a data set, where n is user-defined. Note This function is not currently implemented in Grist.","title":"SMALL"},{"location":"functions/#standardize","text":"STANDARDIZE (value, mean, standard_deviation) # Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution. Note This function is not currently implemented in Grist.","title":"STANDARDIZE"},{"location":"functions/#stdev","text":"STDEV (value, *more_values) # Calculates the standard deviation based on a sample, ignoring non-numerical values. >>> STDEV([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.277849927241488 >>> STDEV([2, 5, 8, 13, 10], 3, 12, 15) 4.810702354423639 >>> STDEV([2, 5, 8, 13, 10], [3, 12, 15]) 4.810702354423639 >>> STDEV([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero","title":"STDEV"},{"location":"functions/#stdeva","text":"STDEVA (value, *more_values) # Calculates the standard deviation based on a sample, setting text to the value 0 . >>> STDEVA([2, 5, 8, 13, 10]) 4.277849927241488 >>> STDEVA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], 1, 0, 0) 4.969550137731641 >>> STDEVA([2, 5, 8, 13, 10], [1, 0, 0]) 4.969550137731641 >>> STDEVA([5]) Traceback (most recent call last): ... ZeroDivisionError: float division by zero","title":"STDEVA"},{"location":"functions/#stdevp","text":"STDEVP (value, *more_values) # Calculates the standard deviation based on an entire population, ignoring non-numerical values. >>> STDEVP([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10, True, False, \"Test\"]) 3.8262252939417984 >>> STDEVP([2, 5, 8, 13, 10], 3, 12, 15) 4.5 >>> STDEVP([2, 5, 8, 13, 10], [3, 12, 15]) 4.5 >>> STDEVP([5]) 0.0","title":"STDEVP"},{"location":"functions/#stdevpa","text":"STDEVPA (value, *more_values) # Calculates the standard deviation based on an entire population, setting text to the value 0 . >>> STDEVPA([2, 5, 8, 13, 10]) 3.8262252939417984 >>> STDEVPA([2, 5, 8, 13, 10, True, False, \"Test\"]) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], 1, 0, 0) 4.648588495446763 >>> STDEVPA([2, 5, 8, 13, 10], [1, 0, 0]) 4.648588495446763 >>> STDEVPA([5]) 0.0","title":"STDEVPA"},{"location":"functions/#steyx","text":"STEYX (data_y, data_x) # Calculates the standard error of the predicted y-value for each x in the regression of a dataset. Note This function is not currently implemented in Grist.","title":"STEYX"},{"location":"functions/#tdist","text":"TDIST (x, degrees_freedom, tails) # Calculates the probability for Student\u2019s t-distribution with a given input (x). Note This function is not currently implemented in Grist.","title":"TDIST"},{"location":"functions/#tinv","text":"TINV (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist.","title":"TINV"},{"location":"functions/#trimmean","text":"TRIMMEAN (data, exclude_proportion) # Calculates the mean of a dataset excluding some proportion of data from the high and low ends of the dataset. Note This function is not currently implemented in Grist.","title":"TRIMMEAN"},{"location":"functions/#ttest","text":"TTEST (range1, range2, tails, type) # Returns the probability associated with t-test. Determines whether two samples are likely to have come from the same two underlying populations that have the same mean. Note This function is not currently implemented in Grist.","title":"TTEST"},{"location":"functions/#t_inv","text":"T_INV (probability, degrees_freedom) # Calculates the negative inverse of the one-tailed TDIST function. Note This function is not currently implemented in Grist.","title":"T_INV"},{"location":"functions/#t_inv_2t","text":"T_INV_2T (probability, degrees_freedom) # Calculates the inverse of the two-tailed TDIST function. Note This function is not currently implemented in Grist.","title":"T_INV_2T"},{"location":"functions/#var","text":"VAR (value1, value2) # Calculates the variance based on a sample. Note This function is not currently implemented in Grist.","title":"VAR"},{"location":"functions/#vara","text":"VARA (value1, value2) # Calculates an estimate of variance based on a sample, setting text to the value 0 . Note This function is not currently implemented in Grist.","title":"VARA"},{"location":"functions/#varp","text":"VARP (value1, value2) # Calculates the variance based on an entire population. Note This function is not currently implemented in Grist.","title":"VARP"},{"location":"functions/#varpa","text":"VARPA (value1, value2) # Calculates the variance based on an entire population, setting text to the value 0 . Note This function is not currently implemented in Grist.","title":"VARPA"},{"location":"functions/#weibull","text":"WEIBULL (x, shape, scale, cumulative) # Returns the value of the Weibull distribution function (or Weibull cumulative distribution function) for a specified shape and scale. Note This function is not currently implemented in Grist.","title":"WEIBULL"},{"location":"functions/#ztest","text":"ZTEST (data, value, standard_deviation) # Returns the two-tailed P-value of a Z-test with standard distribution. Note This function is not currently implemented in Grist.","title":"ZTEST"},{"location":"functions/#text","text":"","title":"Text"},{"location":"functions/#char","text":"CHAR (table_number) # Convert a number into a character according to the current Unicode table. Same as unichr(number) . >>> CHAR(65) u'A' >>> CHAR(33) u'!'","title":"CHAR"},{"location":"functions/#clean","text":"CLEAN (text) # Returns the text with the non-printable characters removed. This removes both characters with values 0 through 31, and other Unicode characters in the \u201ccontrol characters\u201d category. >>> CLEAN(CHAR(9) + \"Monthly report\" + CHAR(10)) u'Monthly report'","title":"CLEAN"},{"location":"functions/#code","text":"CODE (string) # Returns the numeric Unicode map value of the first character in the string provided. Same as ord(string[0]) . >>> CODE(\"A\") 65 >>> CODE(\"!\") 33 >>> CODE(\"!A\") 33","title":"CODE"},{"location":"functions/#concat","text":"CONCAT (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCATENATE . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCAT(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCAT(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCAT(\"abc\") u'abc' >>> CONCAT(0, \"abc\") u'0abc' >>> assert CONCAT(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e'","title":"CONCAT"},{"location":"functions/#concatenate","text":"CONCATENATE (string, *more_strings) # Joins together any number of text strings into one string. Also available under the name CONCAT . Similar to the Python expression \"\".join(array_of_strings) . >>> CONCATENATE(\"Stream population for \", \"trout\", \" \", \"species\", \" is \", 32, \"/mile.\") u'Stream population for trout species is 32/mile.' >>> CONCATENATE(\"In \", 4, \" days it is \", datetime.date(2016,1,1)) u'In 4 days it is 2016-01-01' >>> CONCATENATE(\"abc\") u'abc' >>> CONCATENATE(0, \"abc\") u'0abc' >>> assert CONCATENATE(2, u\" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", u\"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e' >>> assert CONCATENATE(2, \" cr\u00e8me \", \"br\u00fbl\u00e9e\") == u'2 cr\u00e8me br\u00fbl\u00e9e'","title":"CONCATENATE"},{"location":"functions/#dollar","text":"DOLLAR (number, decimals=2) # Formats a number into a formatted dollar amount, with decimals rounded to the specified place (. If decimals value is omitted, it defaults to 2. >>> DOLLAR(1234.567) '$1,234.57' >>> DOLLAR(1234.567, -2) '$1,200' >>> DOLLAR(-1234.567, -2) '($1,200)' >>> DOLLAR(-0.123, 4) '($0.1230)' >>> DOLLAR(99.888) '$99.89' >>> DOLLAR(0) '$0.00' >>> DOLLAR(10, 0) '$10'","title":"DOLLAR"},{"location":"functions/#exact","text":"EXACT (string1, string2) # Tests whether two strings are identical. Same as string2 == string2 . >>> EXACT(\"word\", \"word\") True >>> EXACT(\"Word\", \"word\") False >>> EXACT(\"w ord\", \"word\") False","title":"EXACT"},{"location":"functions/#find","text":"FIND (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> FIND(\"M\", \"Miriam McGovern\") 1 >>> FIND(\"m\", \"Miriam McGovern\") 6 >>> FIND(\"M\", \"Miriam McGovern\", 3) 8 >>> FIND(\" #\", \"Hello world # Test\") 12 >>> FIND(\"gle\", \"Google\", 1) 4 >>> FIND(\"GLE\", \"Google\", 1) Traceback (most recent call last): ... ValueError: substring not found >>> FIND(\"page\", \"homepage\") 5 >>> FIND(\"page\", \"homepage\", 6) Traceback (most recent call last): ... ValueError: substring not found","title":"FIND"},{"location":"functions/#fixed","text":"FIXED (number, decimals=2, no_commas=False) # Formats a number with a fixed number of decimal places (2 by default), and commas. If no_commas is True, then omits the commas. >>> FIXED(1234.567, 1) '1,234.6' >>> FIXED(1234.567, -1) '1,230' >>> FIXED(-1234.567, -1, True) '-1230' >>> FIXED(44.332) '44.33' >>> FIXED(3521.478, 2, False) '3,521.48' >>> FIXED(-3521.478, 1, True) '-3521.5' >>> FIXED(3521.478, 0, True) '3521' >>> FIXED(3521.478, -2, True) '3500'","title":"FIXED"},{"location":"functions/#left","text":"LEFT (string, num_chars=1) # Returns a substring of length num_chars from the beginning of the given string. If num_chars is omitted, it is assumed to be 1. Same as string[:num_chars] . >>> LEFT(\"Sale Price\", 4) 'Sale' >>> LEFT('Swededn') 'S' >>> LEFT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid","title":"LEFT"},{"location":"functions/#len","text":"LEN (text) # Returns the number of characters in a text string, or the number of items in a list. Same as len in python. See Record Set for an example of using len on a list of records. >>> LEN(\"Phoenix, AZ\") 11 >>> LEN(\"\") 0 >>> LEN(\" One \") 11","title":"LEN"},{"location":"functions/#lower","text":"LOWER (text) # Converts a specified string to lowercase. Same as text.lower() . >>> LOWER(\"E. E. Cummings\") 'e. e. cummings' >>> LOWER(\"Apt. 2B\") 'apt. 2b'","title":"LOWER"},{"location":"functions/#mid","text":"MID (text, start_num, num_chars) # Returns a segment of a string, starting at start_num. The first character in text has start_num 1. >>> MID(\"Fluid Flow\", 1, 5) 'Fluid' >>> MID(\"Fluid Flow\", 7, 20) 'Flow' >>> MID(\"Fluid Flow\", 20, 5) '' >>> MID(\"Fluid Flow\", 0, 5) Traceback (most recent call last): ... ValueError: start_num invalid","title":"MID"},{"location":"functions/#phone_format","text":"PHONE_FORMAT (value, country=None, format=None) # Formats a phone number. With no optional arguments, the number must start with \u201c+\u201d and the international dialing prefix, and will be formatted as an international number, e.g. +12345678901 becomes +1 234-567-8901 . The country argument allows specifying a 2-letter country code (e.g. \u201cUS\u201d or \u201cGB\u201d) for interpreting phone numbers that don\u2019t start with \u201c+\u201d. E.g. PHONE_FORMAT('2025555555', 'US') would be seen as a US number and formatted as \u201c(202) 555-5555\u201d. Phone numbers that start with \u201c+\u201d ignore country . E.g. PHONE_FORMAT('+33555555555', 'US') is a French number because \u2018+33\u2019 is the international prefix for France. The format argument specifies the output format, according to this table: \"#\" or \"NATL\" (default) - use the national format, without the international dialing prefix, when possible. E.g. (234) 567-8901 for \u201cUS\u201d, or 02 34 56 78 90 for \u201cFR\u201d. If country is omitted, or the number does not correspond to the given country, the international format is used instead. \"+\" or \"INTL\" - international format, e.g. +1 234-567-8901 or +33 2 34 56 78 90 . \"*\" or \"E164\" - E164 format, like international but with no separators, e.g. +12345678901 . \"tel\" or \"RFC3966\" - format suitable to use as a hyperlink , e.g. \u2018tel:+1-234-567-8901\u2019. When specifying the format argument, you may omit the country argument. I.e. PHONE_FORMAT(value, \"tel\") is equivalent to PHONE_FORMAT(value, None, \"tel\") . For more details, see the phonenumbers Python library, which underlies this function. >>> PHONE_FORMAT(\"+12345678901\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"2345678901\", \"US\") u'(234) 567-8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"2345678901\", \"GB\", \"+\") u'+44 23 4567 8901' >>> PHONE_FORMAT(\"+442345678901\", \"GB\") u'023 4567 8901' >>> PHONE_FORMAT(\"+12345678901\", \"GB\") u'+1 234-567-8901' >>> PHONE_FORMAT(\"(234) 567-8901\") Traceback (most recent call last): ... NumberParseException: (0) Missing or invalid default region. >>> PHONE_FORMAT(\"(234)567 89-01\", \"US\", \"tel\") u'tel:+1-234-567-8901' >>> PHONE_FORMAT(\"2/3456/7890\", \"FR\", '#') u'02 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", '#') u'+33 2 34 56 78 90' >>> PHONE_FORMAT(\"+33234567890\", 'tel') u'tel:+33-2-34-56-78-90' >>> PHONE_FORMAT(\"tel:+1-234-567-8901\", country=\"US\", format=\"*\") u'+12345678901'","title":"PHONE_FORMAT"},{"location":"functions/#proper","text":"PROPER (text) # Capitalizes each word in a specified string. It converts the first letter of each word to uppercase, and all other letters to lowercase. Same as text.title() . >>> PROPER('this is a TITLE') 'This Is A Title' >>> PROPER('2-way street') '2-Way Street' >>> PROPER('76BudGet') '76Budget'","title":"PROPER"},{"location":"functions/#regexextract","text":"REGEXEXTRACT (text, regular_expression) # Extracts the first part of text that matches regular_expression. >>> REGEXEXTRACT(\"Google Doc 101\", \"[0-9]+\") '101' >>> REGEXEXTRACT(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") '826.25' If there is a parenthesized expression, it is returned instead of the whole match. >>> REGEXEXTRACT(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") 'Content' >>> REGEXEXTRACT(\"Foo\", \"Bar\") Traceback (most recent call last): ... ValueError: REGEXEXTRACT text does not match","title":"REGEXEXTRACT"},{"location":"functions/#regexmatch","text":"REGEXMATCH (text, regular_expression) # Returns whether a piece of text matches a regular expression. >>> REGEXMATCH(\"Google Doc 101\", \"[0-9]+\") True >>> REGEXMATCH(\"Google Doc\", \"[0-9]+\") False >>> REGEXMATCH(\"The price today is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\") True >>> REGEXMATCH(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\") True >>> REGEXMATCH(\"Foo\", \"Bar\") False","title":"REGEXMATCH"},{"location":"functions/#regexreplace","text":"REGEXREPLACE (text, regular_expression, replacement) # Replaces all parts of text matching the given regular expression with replacement text. >>> REGEXREPLACE(\"Google Doc 101\", \"[0-9]+\", \"777\") 'Google Doc 777' >>> REGEXREPLACE(\"Google Doc\", \"[0-9]+\", \"777\") 'Google Doc' >>> REGEXREPLACE(\"The price is $826.25\", \"[0-9]*\\.[0-9]+[0-9]+\", \"315.75\") 'The price is $315.75' >>> REGEXREPLACE(\"(Content) between brackets\", \"\\(([A-Za-z]+)\\)\", \"Word\") 'Word between brackets' >>> REGEXREPLACE(\"Foo\", \"Bar\", \"Baz\") 'Foo'","title":"REGEXREPLACE"},{"location":"functions/#replace","text":"REPLACE (text, position, length, new_text) # Replaces part of a text string with a different text string. Position is counted from 1. >>> REPLACE(\"abcdefghijk\", 6, 5, \"*\") 'abcde*k' >>> REPLACE(\"2009\", 3, 2, \"10\") '2010' >>> REPLACE('123456', 1, 3, '@') '@456' >>> REPLACE('foo', 1, 0, 'bar') 'barfoo' >>> REPLACE('foo', 0, 1, 'bar') Traceback (most recent call last): ... ValueError: position invalid","title":"REPLACE"},{"location":"functions/#rept","text":"REPT (text, number_times) # Returns specified text repeated a number of times. Same as text * number_times . The result of the REPT function cannot be longer than 32767 characters, or it raises a ValueError. >>> REPT(\"*-\", 3) '*-*-*-' >>> REPT('-', 10) '----------' >>> REPT('-', 0) '' >>> len(REPT('---', 10000)) 30000 >>> REPT('---', 11000) Traceback (most recent call last): ... ValueError: number_times invalid >>> REPT('-', -1) Traceback (most recent call last): ... ValueError: number_times invalid","title":"REPT"},{"location":"functions/#right","text":"RIGHT (string, num_chars=1) # Returns a substring of length num_chars from the end of a specified string. If num_chars is omitted, it is assumed to be 1. Same as string[-num_chars:] . >>> RIGHT(\"Sale Price\", 5) 'Price' >>> RIGHT('Stock Number') 'r' >>> RIGHT('Text', 100) 'Text' >>> RIGHT('Text', -1) Traceback (most recent call last): ... ValueError: num_chars invalid","title":"RIGHT"},{"location":"functions/#search","text":"SEARCH (find_text, within_text, start_num=1) # Returns the position at which a string is first found within text, ignoring case. Find is case-sensitive. The returned position is 1 if within_text starts with find_text. Start_num specifies the character at which to start the search, defaulting to 1 (the first character of within_text). If find_text is not found, or start_num is invalid, raises ValueError. >>> SEARCH(\"e\", \"Statements\", 6) 7 >>> SEARCH(\"margin\", \"Profit Margin\") 8 >>> SEARCH(\" \", \"Profit Margin\") 7 >>> SEARCH('\"', 'The \"boss\" is here.') 5 >>> SEARCH(\"gle\", \"Google\") 4 >>> SEARCH(\"GLE\", \"Google\") 4","title":"SEARCH"},{"location":"functions/#substitute","text":"SUBSTITUTE (text, old_text, new_text, instance_num=None) # Replaces existing text with new text in a string. It is useful when you know the substring of text to replace. Use REPLACE when you know the position of text to replace. If instance_num is given, it specifies which occurrence of old_text to replace. If omitted, all occurrences are replaced. Same as text.replace(old_text, new_text) when instance_num is omitted. >>> SUBSTITUTE(\"Sales Data\", \"Sales\", \"Cost\") u'Cost Data' >>> SUBSTITUTE(\"Quarter 1, 2008\", \"1\", \"2\", 1) u'Quarter 2, 2008' >>> SUBSTITUTE(\"Quarter 1, 2011\", \"1\", \"2\", 3) u'Quarter 1, 2012'","title":"SUBSTITUTE"},{"location":"functions/#t","text":"T (value) # Returns value if value is text, or the empty string when value is not text. >>> T('Text') u'Text' >>> T(826) u'' >>> T('826') u'826' >>> T(False) u'' >>> T('100 points') u'100 points' >>> T(AltText('Text')) u'Text' >>> T(float('nan')) u''","title":"T"},{"location":"functions/#text_1","text":"TEXT (number, format_type) # Converts a number into text according to a specified format. It is not yet implemented in Grist. Note This function is not currently implemented in Grist.","title":"TEXT"},{"location":"functions/#trim","text":"TRIM (text) # Removes all spaces from text except for single spaces between words. Note that TRIM does not remove other whitespace such as tab or newline characters. >>> TRIM(\" First Quarter\\n Earnings \") 'First Quarter\\n Earnings' >>> TRIM(\"\") ''","title":"TRIM"},{"location":"functions/#upper","text":"UPPER (text) # Converts a specified string to uppercase. Same as text.upper() . >>> UPPER(\"e. e. cummings\") 'E. E. CUMMINGS' >>> UPPER(\"Apt. 2B\") 'APT. 2B'","title":"UPPER"},{"location":"functions/#value","text":"VALUE (text) # Converts a string in accepted date, time or number formats into a number or date. >>> VALUE(\"$1,000\") 1000 >>> assert VALUE(\"16:48:00\") - VALUE(\"12:00:00\") == datetime.timedelta(0, 17280) >>> VALUE(\"01/01/2012\") datetime.datetime(2012, 1, 1, 0, 0) >>> VALUE(\"\") 0 >>> VALUE(0) 0 >>> VALUE(\"826\") 826 >>> VALUE(\"-826.123123123\") -826.123123123 >>> VALUE(float('nan')) nan >>> VALUE(\"Invalid\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number >>> VALUE(\"13/13/13\") Traceback (most recent call last): ... ValueError: text cannot be parsed to a number","title":"VALUE"},{"location":"glossary/","text":"Glossary # Bar Chart # This is a classic chart type , where the values in a column are shown as the heights of a series of rectangles. Column # A column is a vertical series of cells in a table. Columns in Grist have names. Each cell in a column is in a different row. When data from a column is present within a card, we call it a field. When a table of data is represented as a chart, we refer to each column as a series. From a data modeling perspective, columns typically have data about a single aspect of many real world entities, whereas rows have data about many aspects of a single entity. Column Options # Every column\u2019s appearance and behavior can be customized by clicking on the column header, clicking on the drop-down, and selecting \u201cColumn Options\u201d. Column Type # Columns have types, which control the appearance of cells in that column and the method used to edit them. You can change the column type at will. The Text Column Type is suited to strings of any length; the Date Column Type is specialized for storing and editing calendar dates; the Reference Column Type is for storing and editing links to other tables; the Numeric Column Type is for any kind of number; etc. Creator Panel # The creator panel is the right-side menu of configuration options for widgets and columns. Dashboard # A dashboard is just another name for a page, typically organized to give a summary or overview of a document\u2019s data. Grist is well suited to constructing dashboards, by creating pages with suitably linked widgets . Data Table # Data is stored in tables. Tables have named columns , and a sequence of rows containing values for those columns. Every row has a numeric id (available as $id in formulas) that is unique within that table. The raw data page lists all data tables in your document. Document # A Grist document is a collection of related data. If you work with databases, think of it as a single database. If you work with spreadsheets, think of it as a single spreadsheet. Like databases and spreadsheets, the data in a Grist document is organized as a set of tables. How this data is viewed and edited is unusually flexible. Grist documents are organized visually into pages. Pages contain widgets that offer different ways to view and edit tables. To work with Grist, the first step is typically to create a document . Drag handle # This is an icon to facilitate reorganizing views or lists visually. On a desktop computer, when hovering over a drag handle, the mouse cursor changes. The drag handle for a widget is just to the left of the widget\u2019s title. There is an example of using this drag handle in the investment research demo . Fiddle mode # Fiddle mode is a special mode that some Grist documents, such as the ones from the Examples & Templates page , will open in. A document opened in fiddle mode will have a \u201cfiddle\u201d label next to the document title in the top bar. In fiddle mode, any edit to a document will cause a new, unsaved copy (a.k.a \u201cfork\u201d) of that document to be created; the original document will remain unaffected. The copy can be saved via the \u201cSave Copy\u201d button or menu option. You can add /m/fork to the end of any document\u2019s URL to make that document open in fiddle mode (e.g. https://public.getgrist.com/3NsoHE2mWtEp/Lead-list/m/fork ). Field # A field is a column shown in a Card Widget. The terms column, field, and series are not different in substance, but are different terms that make more sense for different widgets. In a Table Widget, we talk about columns. In a Chart Widget, we talk about series. And in a Card Widget, we talk about fields. A field has layout properties that are meaningful within a Card, but would not be meaningful in other contexts. Import # To import into Grist means to take data from other sources (on your computer or on the internet) and place that data in a Grist document. Examples of importing include: Take a CSV file on your computer, and create a Grist document with the same content (see: start a new Grist document ). Take an Excel file on your computer, and add the data from it to an existing Grist document (see: importing more data ). Take a JSON file on the internet, and add the data from it to an existing Grist document (see: importing more data ). Calling Grist\u2019s API from a program and adding data read from another source (see: Grist API ). Lookups # Lookup formulas allow you to \u201clook up\u201d data in other tables. lookupOne allows you to look up a single record in another table by matching some data across two tables, similar to Excel\u2019s VLOOKUP. lookupRecords allows you to look up multiple records in another table by matching some data across two tables. Lookups can be combined with dot notation to pull data from referenced records. Learn how. Page # Grist documents are organized visually into pages. Each page allows you to view or edit one or more tables. The nature of these viewers and editors (called \u201cpage widgets\u201d) is flexible, as is their layout. A single table can be viewed (or edited) from multiple widgets in one or many pages. And a single page can contain widgets for viewing (or editing) many tables. Pages are listed in the document ( in the panel on the left ). In this list, they may be rearranged and grouped, with several \u201csubpages\u201d nested within a single page. Pie Chart # This is a classic chart type , where a circle is sliced up according to values in a column. Record # A record is the data in one row of a table, comprising the data in the individual cells of that row. It has a unique identifier, usually hidden but available in formulas as id . In a Card Widget or a Card List Widget, a record is represented by a single card. Row # A horizontal series of cells in a table. Each cell in a row belongs to a different column. The data stored in a row is also called a record. Typically rows have data about different aspects a single entity, whereas columns have data about a single aspect of many entities. Series # Data from a single column shown in a Chart Widget is called a series. The same data in a Card Widget is called a field, and in a Table Widget is called a column. Sort # The order in which rows of a table are shown is called the sort order. An example of changing the sort order of a table is given in the CRM tutorial . Trigger Formulas # A trigger formula is a formula that recalculates your data based on a set of conditions that you decide. They also allow you to clean data when a new value is entered ( watch webinar ), provide a sensible default value for a column or create Time and Authorship stamps . User Menu # The user menu is the menu that opens by clicking your profile icon in the top-right of Grist. From there you can manage your profile, add additional Grist accounts that you own, and see a list of team sites to which you have access. Depending on where you are, the user menu will contain additional options. For example, from a personal site you\u2019ll see the option to upgrade your plan to a team site . From a team site, depending on your role and permissions, you may be able to manage billing or edit team members . From a document, you\u2019ll find an option to edit document settings. Widget # A page contains sections, such as table grids or charts, which we call \u201cpage widgets\u201d. They are used for viewing or editing data in tables. Types of page widgets include Card Widgets , Chart Widgets , as well as the classic spreadsheet-style table grid (called a Table Widget ). Widget Options # Every page widget can have its appearance and behavior customized. How this is done varies between widget, but can always be done systematically by clicking on the three-dot menu on the top right of a widget and selecting \u201cWidget options\u201d. Wrap Text # Normally, content that doesn\u2019t fit in the width of a cell is truncated, with \u201c\u2026\u201d indicating that part of the data is hidden. When \u201cWrap Text\u201d setting is enabled, long lines will wrap, and the cell will get taller to include all content. An example of wrapping is given in the CRM tutorial .","title":"Glossary"},{"location":"glossary/#glossary","text":"","title":"Glossary"},{"location":"glossary/#bar-chart","text":"This is a classic chart type , where the values in a column are shown as the heights of a series of rectangles.","title":"Bar Chart"},{"location":"glossary/#column","text":"A column is a vertical series of cells in a table. Columns in Grist have names. Each cell in a column is in a different row. When data from a column is present within a card, we call it a field. When a table of data is represented as a chart, we refer to each column as a series. From a data modeling perspective, columns typically have data about a single aspect of many real world entities, whereas rows have data about many aspects of a single entity.","title":"Column"},{"location":"glossary/#column-options","text":"Every column\u2019s appearance and behavior can be customized by clicking on the column header, clicking on the drop-down, and selecting \u201cColumn Options\u201d.","title":"Column Options"},{"location":"glossary/#column-type","text":"Columns have types, which control the appearance of cells in that column and the method used to edit them. You can change the column type at will. The Text Column Type is suited to strings of any length; the Date Column Type is specialized for storing and editing calendar dates; the Reference Column Type is for storing and editing links to other tables; the Numeric Column Type is for any kind of number; etc.","title":"Column Type"},{"location":"glossary/#creator-panel","text":"The creator panel is the right-side menu of configuration options for widgets and columns.","title":"Creator Panel"},{"location":"glossary/#dashboard","text":"A dashboard is just another name for a page, typically organized to give a summary or overview of a document\u2019s data. Grist is well suited to constructing dashboards, by creating pages with suitably linked widgets .","title":"Dashboard"},{"location":"glossary/#data-table","text":"Data is stored in tables. Tables have named columns , and a sequence of rows containing values for those columns. Every row has a numeric id (available as $id in formulas) that is unique within that table. The raw data page lists all data tables in your document.","title":"Data Table"},{"location":"glossary/#document","text":"A Grist document is a collection of related data. If you work with databases, think of it as a single database. If you work with spreadsheets, think of it as a single spreadsheet. Like databases and spreadsheets, the data in a Grist document is organized as a set of tables. How this data is viewed and edited is unusually flexible. Grist documents are organized visually into pages. Pages contain widgets that offer different ways to view and edit tables. To work with Grist, the first step is typically to create a document .","title":"Document"},{"location":"glossary/#drag-handle","text":"This is an icon to facilitate reorganizing views or lists visually. On a desktop computer, when hovering over a drag handle, the mouse cursor changes. The drag handle for a widget is just to the left of the widget\u2019s title. There is an example of using this drag handle in the investment research demo .","title":"Drag handle"},{"location":"glossary/#fiddle-mode","text":"Fiddle mode is a special mode that some Grist documents, such as the ones from the Examples & Templates page , will open in. A document opened in fiddle mode will have a \u201cfiddle\u201d label next to the document title in the top bar. In fiddle mode, any edit to a document will cause a new, unsaved copy (a.k.a \u201cfork\u201d) of that document to be created; the original document will remain unaffected. The copy can be saved via the \u201cSave Copy\u201d button or menu option. You can add /m/fork to the end of any document\u2019s URL to make that document open in fiddle mode (e.g. https://public.getgrist.com/3NsoHE2mWtEp/Lead-list/m/fork ).","title":"Fiddle mode"},{"location":"glossary/#field","text":"A field is a column shown in a Card Widget. The terms column, field, and series are not different in substance, but are different terms that make more sense for different widgets. In a Table Widget, we talk about columns. In a Chart Widget, we talk about series. And in a Card Widget, we talk about fields. A field has layout properties that are meaningful within a Card, but would not be meaningful in other contexts.","title":"Field"},{"location":"glossary/#import","text":"To import into Grist means to take data from other sources (on your computer or on the internet) and place that data in a Grist document. Examples of importing include: Take a CSV file on your computer, and create a Grist document with the same content (see: start a new Grist document ). Take an Excel file on your computer, and add the data from it to an existing Grist document (see: importing more data ). Take a JSON file on the internet, and add the data from it to an existing Grist document (see: importing more data ). Calling Grist\u2019s API from a program and adding data read from another source (see: Grist API ).","title":"Import"},{"location":"glossary/#lookups","text":"Lookup formulas allow you to \u201clook up\u201d data in other tables. lookupOne allows you to look up a single record in another table by matching some data across two tables, similar to Excel\u2019s VLOOKUP. lookupRecords allows you to look up multiple records in another table by matching some data across two tables. Lookups can be combined with dot notation to pull data from referenced records. Learn how.","title":"Lookups"},{"location":"glossary/#page","text":"Grist documents are organized visually into pages. Each page allows you to view or edit one or more tables. The nature of these viewers and editors (called \u201cpage widgets\u201d) is flexible, as is their layout. A single table can be viewed (or edited) from multiple widgets in one or many pages. And a single page can contain widgets for viewing (or editing) many tables. Pages are listed in the document ( in the panel on the left ). In this list, they may be rearranged and grouped, with several \u201csubpages\u201d nested within a single page.","title":"Page"},{"location":"glossary/#pie-chart","text":"This is a classic chart type , where a circle is sliced up according to values in a column.","title":"Pie Chart"},{"location":"glossary/#record","text":"A record is the data in one row of a table, comprising the data in the individual cells of that row. It has a unique identifier, usually hidden but available in formulas as id . In a Card Widget or a Card List Widget, a record is represented by a single card.","title":"Record"},{"location":"glossary/#row","text":"A horizontal series of cells in a table. Each cell in a row belongs to a different column. The data stored in a row is also called a record. Typically rows have data about different aspects a single entity, whereas columns have data about a single aspect of many entities.","title":"Row"},{"location":"glossary/#series","text":"Data from a single column shown in a Chart Widget is called a series. The same data in a Card Widget is called a field, and in a Table Widget is called a column.","title":"Series"},{"location":"glossary/#sort","text":"The order in which rows of a table are shown is called the sort order. An example of changing the sort order of a table is given in the CRM tutorial .","title":"Sort"},{"location":"glossary/#trigger-formulas","text":"A trigger formula is a formula that recalculates your data based on a set of conditions that you decide. They also allow you to clean data when a new value is entered ( watch webinar ), provide a sensible default value for a column or create Time and Authorship stamps .","title":"Trigger Formulas"},{"location":"glossary/#user-menu","text":"The user menu is the menu that opens by clicking your profile icon in the top-right of Grist. From there you can manage your profile, add additional Grist accounts that you own, and see a list of team sites to which you have access. Depending on where you are, the user menu will contain additional options. For example, from a personal site you\u2019ll see the option to upgrade your plan to a team site . From a team site, depending on your role and permissions, you may be able to manage billing or edit team members . From a document, you\u2019ll find an option to edit document settings.","title":"User Menu"},{"location":"glossary/#widget","text":"A page contains sections, such as table grids or charts, which we call \u201cpage widgets\u201d. They are used for viewing or editing data in tables. Types of page widgets include Card Widgets , Chart Widgets , as well as the classic spreadsheet-style table grid (called a Table Widget ).","title":"Widget"},{"location":"glossary/#widget-options","text":"Every page widget can have its appearance and behavior customized. How this is done varies between widget, but can always be done systematically by clicking on the three-dot menu on the top right of a widget and selecting \u201cWidget options\u201d.","title":"Widget Options"},{"location":"glossary/#wrap-text","text":"Normally, content that doesn\u2019t fit in the width of a cell is truncated, with \u201c\u2026\u201d indicating that part of the data is hidden. When \u201cWrap Text\u201d setting is enabled, long lines will wrap, and the cell will get taller to include all content. An example of wrapping is given in the CRM tutorial .","title":"Wrap Text"},{"location":"imports/","text":"Importing more data # You can import a file to start a new Grist document , or to add data to an existing document. Grist supports imports of Excel, CSV, JSON, tab-separated files and from Google Drive . To start a new Grist document, click the \u201cAdd New\u201d button on the home screen and choose \u201cImport document\u201d, as described in starting a new Grist document . To add to an existing document, open that document, click the \u201cAdd New\u201d button and then \u201cImport from file\u201d. By default, each imported table is added as a new Grist table, but when examining the preview dialog for an import, you have an option to change the destination to an existing Grist table. You can also import any of the same formats from a URL, using the \u201cImport from URL\u201d option. The Import dialog # When you import data into an existing document, Grist opens an import dialog to show you what will be imported. This dialog offers available import options, lets you choose whether to create a new table or add to an existing one, and shows a preview of the data. The \u201cImport options\u201d link on the top right is sometimes useful when importing delimited files. Grist guesses the settings to parse the data (such as the field delimiter), but if it guesses incorrectly, you can adjust the settings. Guessing data structure # In all cases, when you import a file, Grist makes guesses about the structure of the file. For Excel files, Grist treats each sheet as a separated table. For CSV and other delimited formats, one file becomes one table. For both Excel and delimited files, Grist tries to detect whether the headers are included and which line they occur in. If Grist detects there are no headers, it will name columns as \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc. Grist automatically tries to parse numbers, dates, and boolean fields to detect the most suitable type for each column. It tries to be lossless: e.g. if it marks a column as numeric, any text values in it (such as \u201cN/A\u201d) will remain in the imported table, but will be highlighted due to the type mismatch. You can always rename tables and columns after an import, as well as convert types. Import from Google Drive # Importing from a Google Drive is as easy as importing from an Excel file or a CSV file. You can either provide an URL of a file stored in the Google Drive or use a Google File Picker to choose a file from your own drive. To use a Picker, click the \u201cAdd New\u201d button and choose \u201cImport from Google Drive\u201d. To import, sign in to your Google Account by clicking the \u201cSign in\u201d button and following the sign-in process. Grist will ask for a permission to read the file you will import from Google Drive. We won\u2019t read any other files on your drive \u2014 just the single file you choose to import. Once the file has been chosen, the rest of the process is the same as importing from an Excel file. In the import dialog you may configure what data to import, and which destination table to add it to. If you have an URL to a file or a spreadsheet stored on your Google Drive or a file that is publicly accessible, you can import it directly using the \u201cImport from URL\u201d option from the \u201cAdd New\u201d menu. If the file is not shared publicly, Grist will ask you for permission to read files from your Google Drive. If you don\u2019t want to allow Grist to read your files, it will open up Google File Picker where you can select the file you want to import. Import to an existing table # By default, Grist imports new data as new tables, but the Import dialog allows you to change the destination and import data into an existing table. When importing to an existing table, Grist will attempt to match the columns from your imported file to the columns in the destination table. To manually specify column matching, click the \u2018Source Column\u2019 drop down to open a menu with a list of unused columns from your imported file. Click on a column name to match it to a destination column, or select \u2018Skip\u2019 to skip importing data from that column. You can also specify a formula for each imported column by clicking \u2018Apply Formula\u2019 from the \u2018Source Column\u2019 drop down menu. Formulas can reference one or more imported columns, and the result of evaluating the formula will be shown in the preview after closing the editor. Importing to an existing table is best suited for importing multiple datasets containing similar structure. For instance, you could import a bank statement as a new table, then import more statements from other months into the same table. For developers, the Grist API offers a more powerful way to add data to a Grist document. Updating existing records # Suppose the table we are importing to already contains some of the data in our file. We\u2019d like Grist to update these existing records rather than duplicate them. We can tell Grist to update these records by checking the \u201cUpdate existing records\u201d option, and specifying which fields to use for matching incoming data against existing records. An imported row and an existing record that have the same values for all of the selected merge fields are considered the same record. In this case, importing will update the fields of the record with the imported data. Blank values from the imported data will be skipped, leaving the corresponding fields in the original record unchanged. Note: If there are multiple imported rows that have the same values for the selected merge fields, the last row will be used for matching and updating. If there are multiple existing records that have the same values for the selected merge fields, only the first record will be updated if a matching imported row is found. Each time the merge fields are changed, Grist will generate a preview of the updates that will be made to the destination table, and display them in the preview table. Changes are highlighted as follows: New records have all of their fields highlighted in green. Updated records have red and green highlighting for any changed fields: red (with a strikethrough) for existing values from the destination, and green for new values from the imported file. Unchanged records have no highlighting. Field values that exist in the destination table, but are blank in the imported file, are distinguished by a light gray font color.","title":"Importing more data"},{"location":"imports/#importing-more-data","text":"You can import a file to start a new Grist document , or to add data to an existing document. Grist supports imports of Excel, CSV, JSON, tab-separated files and from Google Drive . To start a new Grist document, click the \u201cAdd New\u201d button on the home screen and choose \u201cImport document\u201d, as described in starting a new Grist document . To add to an existing document, open that document, click the \u201cAdd New\u201d button and then \u201cImport from file\u201d. By default, each imported table is added as a new Grist table, but when examining the preview dialog for an import, you have an option to change the destination to an existing Grist table. You can also import any of the same formats from a URL, using the \u201cImport from URL\u201d option.","title":"Importing more data"},{"location":"imports/#the-import-dialog","text":"When you import data into an existing document, Grist opens an import dialog to show you what will be imported. This dialog offers available import options, lets you choose whether to create a new table or add to an existing one, and shows a preview of the data. The \u201cImport options\u201d link on the top right is sometimes useful when importing delimited files. Grist guesses the settings to parse the data (such as the field delimiter), but if it guesses incorrectly, you can adjust the settings.","title":"The Import dialog"},{"location":"imports/#guessing-data-structure","text":"In all cases, when you import a file, Grist makes guesses about the structure of the file. For Excel files, Grist treats each sheet as a separated table. For CSV and other delimited formats, one file becomes one table. For both Excel and delimited files, Grist tries to detect whether the headers are included and which line they occur in. If Grist detects there are no headers, it will name columns as \u201cA\u201d, \u201cB\u201d, \u201cC\u201d, etc. Grist automatically tries to parse numbers, dates, and boolean fields to detect the most suitable type for each column. It tries to be lossless: e.g. if it marks a column as numeric, any text values in it (such as \u201cN/A\u201d) will remain in the imported table, but will be highlighted due to the type mismatch. You can always rename tables and columns after an import, as well as convert types.","title":"Guessing data structure"},{"location":"imports/#import-from-google-drive","text":"Importing from a Google Drive is as easy as importing from an Excel file or a CSV file. You can either provide an URL of a file stored in the Google Drive or use a Google File Picker to choose a file from your own drive. To use a Picker, click the \u201cAdd New\u201d button and choose \u201cImport from Google Drive\u201d. To import, sign in to your Google Account by clicking the \u201cSign in\u201d button and following the sign-in process. Grist will ask for a permission to read the file you will import from Google Drive. We won\u2019t read any other files on your drive \u2014 just the single file you choose to import. Once the file has been chosen, the rest of the process is the same as importing from an Excel file. In the import dialog you may configure what data to import, and which destination table to add it to. If you have an URL to a file or a spreadsheet stored on your Google Drive or a file that is publicly accessible, you can import it directly using the \u201cImport from URL\u201d option from the \u201cAdd New\u201d menu. If the file is not shared publicly, Grist will ask you for permission to read files from your Google Drive. If you don\u2019t want to allow Grist to read your files, it will open up Google File Picker where you can select the file you want to import.","title":"Import from Google Drive"},{"location":"imports/#import-to-an-existing-table","text":"By default, Grist imports new data as new tables, but the Import dialog allows you to change the destination and import data into an existing table. When importing to an existing table, Grist will attempt to match the columns from your imported file to the columns in the destination table. To manually specify column matching, click the \u2018Source Column\u2019 drop down to open a menu with a list of unused columns from your imported file. Click on a column name to match it to a destination column, or select \u2018Skip\u2019 to skip importing data from that column. You can also specify a formula for each imported column by clicking \u2018Apply Formula\u2019 from the \u2018Source Column\u2019 drop down menu. Formulas can reference one or more imported columns, and the result of evaluating the formula will be shown in the preview after closing the editor. Importing to an existing table is best suited for importing multiple datasets containing similar structure. For instance, you could import a bank statement as a new table, then import more statements from other months into the same table. For developers, the Grist API offers a more powerful way to add data to a Grist document.","title":"Import to an existing table"},{"location":"imports/#updating-existing-records","text":"Suppose the table we are importing to already contains some of the data in our file. We\u2019d like Grist to update these existing records rather than duplicate them. We can tell Grist to update these records by checking the \u201cUpdate existing records\u201d option, and specifying which fields to use for matching incoming data against existing records. An imported row and an existing record that have the same values for all of the selected merge fields are considered the same record. In this case, importing will update the fields of the record with the imported data. Blank values from the imported data will be skipped, leaving the corresponding fields in the original record unchanged. Note: If there are multiple imported rows that have the same values for the selected merge fields, the last row will be used for matching and updating. If there are multiple existing records that have the same values for the selected merge fields, only the first record will be updated if a matching imported row is found. Each time the merge fields are changed, Grist will generate a preview of the updates that will be made to the destination table, and display them in the preview table. Changes are highlighted as follows: New records have all of their fields highlighted in green. Updated records have red and green highlighting for any changed fields: red (with a strikethrough) for existing values from the destination, and green for new values from the imported file. Unchanged records have no highlighting. Field values that exist in the destination table, but are blank in the imported file, are distinguished by a light gray font color.","title":"Updating existing records"},{"location":"integrators/","text":"Integrator Services # Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make Configuring Integrators # Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables. Example: Storing form submissions # Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in! Example: Sending email alerts # We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win! Readiness column # Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example . Triggering (or avoiding triggering) on pre-existing records # The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Integrator Services"},{"location":"integrators/#integrator-services","text":"Grist can be connected to thousands of other services via integrators with Grist support. These include: Zapier Integrately Pabbly Connect KonnectzIT n8n Make","title":"Integrator Services"},{"location":"integrators/#configuring-integrators","text":"Each integrator provides its own way to configure the connection between Grist and other services. Pabbly Connect has created a few videos walking through how to set up an integration with Grist using Pabbly Connect. Pabbly Connect Youtube - Grist Playlist Included below is a walkthrough of an example of how an integration with Grist can be configured using Zapier . Grist can trigger a workflow whenever there is a new or updated record in a table, leading to action in another service. Conversely, workflows triggered by other services can consult, add, or update records in Grist tables.","title":"Configuring Integrators"},{"location":"integrators/#example-storing-form-submissions","text":"Suppose we have a form for collecting votes on the color of a proposed new bike shed: The form is set up using Google Forms (for this example), and we want the responses to be stored in a Grist document: One way to make this happen is with Zapier . So let\u2019s sign in on the Zapier site and then visit the Grist integration page : We\u2019d like to pair Grist with Google forms. Zapier supports several form providers, and the overall process for integration is similar for them all. Just type in the provider you want. For this tutorial, we\u2019re going with Google forms. Once we\u2019ve picked the provider to integrate with, we need to pin down exactly what we want it to do, from the available \u201ctriggers\u201d and \u201cactions.\u201d In this case, we choose that when there is a New Response in Spreadsheet trigger for Google Forms, we will do the Create Record action in Grist. We click the build button to start filling in the details: Since the triggering event for the integration will happen in Google Forms, we are first asked to give Zapier some access rights to your forms. Once that is done, we are prompted to confirm which spreadsheet to use: Then we specify which worksheet within the spreadsheet to use (easy if there is just one). For Zapier\u2019s benefit in a later step, it is important that there be at least one sample response already in the spreadsheet. That\u2019s the Google side done. Now for the Grist side. We are prompted to give an API key for Grist, so we set up an API key if we haven\u2019t already. To give precise access rights, we could set up a user account just for the integration, and give it access to just what it needs, and supply its API key. Now we confirm the team to use - personal docs or a team site we have access to: Then we pick the Grist document to send form responses to: And then we pick the table to use within that document. It should have columns to store whatever parts of the form we want to keep. It is important to make this table if it doesn\u2019t exist already; it won\u2019t be created automatically. It isn\u2019t important to match column names with questions. Zapier allows for flexible mapping of fields between services. In our case, a one-to-one mapping works fine: Ok! Now we can click our button to have Zap test our integration. All going well, we can turn the \u201cZap\u201d on and leave it run. Now is the time to try making some submissions, and go have a cup of something. Free \u201cZaps\u201d may run periodically to check for new submissions, so don\u2019t expect immediate results in all cases. But eventually, you should see the votes pouring in!","title":"Example: Storing form submissions"},{"location":"integrators/#example-sending-email-alerts","text":"We\u2019ve seen an example of an outside service sending data to Grist. Now let\u2019s look at an example of Grist sending data to an outside service. Continuing our form example, where a Grist document is accumulating votes for a preferred color: now suppose that every time a new vote comes in we want to send an email summarizing which option is in the lead. We write a formula to prepare the text in a Text cell: Let\u2019s return again to the Grist integration page on Zapier. There are several mail integrations. For this example, we pick Gmail: Once we\u2019ve picked the service to connect, now we choose exactly what we want it to do. In this case, we choose that when there is a New or Updated Record (Instant) in Grist, we will Send Email in Gmail. Note the Instant there. Triggers in Zapier can be either a regular kind where Zapier periodically checks for changes (this is relatively slow), or a special \u201cinstant\u201d kind that needs special support from the triggering service but is a lot faster. Grist supports either kind of trigger, and we strongly recommend \u201cinstant\u201d if you prefer results in seconds rather than minutes, and especially if you are the sort to get anxious if someone doesn\u2019t respond to your IMs immediately. Once we\u2019ve chosen a Grist account to use as before, we can pick a table within a document to monitor. For instant triggers, we can optionally specify a \u201creadiness\u201d column . If we leave this blank, anytime a record is created or changed in the selected table, Grist will notify Zapier about it. If we set it, it should generally be to a toggle column , and Grist will notify Zapier only for records when that column is turned on. That is handy for records that have many columns being filled in manually, when we don\u2019t want to trigger until they are complete. For this example, it is fine to leave the readiness column blank. (For regular non-instant triggers, we would need to pick a specific column to monitor. Ideally this would be an Updated At column, see Timestamp columns ). On the Gmail side, we can email to pre-set addresses, or this could be configured dynamically (we\u2019ll see an example of how in a moment): We choose to set the body of the email to contain \u201cCustom\u201d content, in this case the Text cell we calculated earlier. And we\u2019re done! Zapier will offer to make a quick test that emails go out correctly: Then you can make some votes and watch the system work. For instant triggers, results should show up fairly snappily. Otherwise, Zapier has a \u201crun zap\u201d functionality to force an integration to update immediately: And emails should start showing up in the desired inboxes. May the best almost indistinguishable shade win!","title":"Example: Sending email alerts"},{"location":"integrators/#readiness-column","text":"Grist has a mechanism for alerting other services when data changes within a document. This serves as the basis for Zapier instant triggers. Since Grist is a spreadsheet, it is common for records (rows) to be created empty, and for cells to then be filled in one by one. This creates an important nuance for notifications. Usually it won\u2019t be desirable to send a notification until the record is in some sense \u201cready\u201d, but when exactly is that? Grist lets the user decide for themselves, by creating a toggle (boolean) column which is turned on when the record is ready. The column can be set manually, or via a formula. This is called a readiness column. For example, if you only want to activate a trigger when columns called Name and Email are not empty, your readiness column can have the following formula: bool($Name and $Email) You would make the column take effect by supplying it in the Readiness column option described in the email alert example .","title":"Readiness column"},{"location":"integrators/#triggering-or-avoiding-triggering-on-pre-existing-records","text":"The order of steps matters when setting up an integration that uses a readiness column. If you have existing data, think through whether you want the integration to affect all existing data or just updates and new data. For example, if you are sending data from Grist to Google Sheets using a Zapier integration, you\u2019ll probably want to send your existing data. In this case, set up and enable your Zap first with an empty readiness column, then turn on all the readiness cells. If you want to send a notification only when something is added to Grist, and not for pre-existing records, make sure your readiness cells are all turned on prior to enabling the integration, otherwise once they are turned on notifications will be sent for all of them. That may be a lot!","title":"Triggering (or avoiding triggering) on pre-existing records"},{"location":"investment-research/","text":"How to analyze and visualize data # Grist offers several powerful ways to analyze and visualize data. In this tutorial, you\u2019ll learn how to: Create summary tables Create and configure charts Link charts dynamically To explain these features, we\u2019ll use the sample document \u201cInvestment Research\u201d 1 which includes companies and investments in them up to 2013. Let\u2019s take a look at the sample document and then we\u2019ll talk about how to build it so that you can apply these tools to your own data. Exploring the example # Open the document \u201c Investment Research \u201d, found in Examples & Templates in your Grist home page. The first thing you\u2019ll see is \u201cOverview\u201d. This page contains two charts next to two tables. The top left has a pie chart showing the distribution of investments by category. The table next to it has the same data in tabular form. Below the pie chart is a bar graph showing the total investments raised by year. It is also accompanied by the same data in the table next to it in tabular form. All these charts and tables are examples of \u201csummary tables\u201d, which we\u2019ll describe below. The next page, \u201cBreakdowns\u201d, also contains two tables and two charts, but these are linked dynamically and offer much more detailed insight into the data. On the top left is a table showing the total funding by year (the same table as we saw on the previous page). This table serves as a driver for the chart next to it. When you click on a year in the table, the pie chart updates to show the distribution of investments in that year. Similarly, the bottom table shows investments by category. When you click on any category, the line chart next to it updates to show the history of funding in that category over the years. Note how powerful this is, and how much insight you can gain from it. For instance, you can see that Advertising category has been getting a lot of investment in NY since 2007, but was overtaken by E-commerce in 2013, while the Fashion category had a major spike in 2011. On the next page, \u201cCompany Details\u201d, we get to see the granular data of this dataset. Here, we see a list of companies and the categories they fall into. Each company shown has a link pointing to its listing on the Crunchbase website. Selecting a company shows a card with its details, as well as a list of all the investments it has received. This is where we begin to see the power of Grist. The original dataset is a flat spreadsheet of companies, and an even bigger spreadsheet of investments. By displaying the data graphically, the data comes alive, making it powerful and useful. How can I make this? # With Grist, presenting your own data in graphic form is a few easy steps away. Let\u2019s begin with the first step. Get the data # Let\u2019s import the raw data. We\u2019ll import two CSV files, where each will become its own table. To follow along, save the files from crunchbase_companies_ny.csv and crunchbase_investments_ny.csv to your computer first. Then, create a Grist document by importing the first file from the home page. Next, import the second table using the \u201cAdd New\u201d button and the \u201cImport from file\u201d option. In the import dialog box, finish by clicking \u201cImport\u201d on the bottom left. The tables you\u2019ve imported will be named \u201ccrunchbase_companies_ny\u201d and \u201ccrunchbase_investments_ny\u201d. Click the name at the top of the table to open the dialogue box and rename each of the tables to \u201cCompanies\u201d and \u201cInvestments\u201d. Make it relational # The power of Grist comes from giving structure to the data. Take a look at the \u201cInvestments\u201d table. Sort by the first column and you\u2019ll notice how much repetition there is: each row contains the complete company info, which both duplicates the data in the \u201cCompanies\u201d table, and is repeated multiple times when multiple investments apply to the same company. 2 The reality is that each investment applies to a single company. Each investment row only needs to contain a reference to a company, and the data specific to that investment. To make it so, find a column that identifies a company uniquely. In this dataset, the first column, \u201ccompany_permalink\u201d, does it best 3 . Click on the arrow in the column header and click \u201cColumn Options\u201d. Click on the arrow beside \u201cText\u201d under the \u201cColumn Type\u201d in the dialogue box at the right of the screen and select \u201cReference\u201d from the list. Grist will automatically suggest to make it a \u201cReference\u201d to the \u201cCompanies\u201d table, and to show the referenced company\u2019s \u201cpermalink\u201d. Click \u201cApply\u201d to save this conversion. Let\u2019s also rename this column to \u201cCompany\u201d. In Grist, duplicated data is not needed and we recommend removing it. Using the Option-Minus (Mac) or Alt-Minus (Windows) shortcut is a quick way to remove columns. After removing the columns from \u201ccompany_name\u201d to \u201ccompany_city\u201d, here\u2019s what\u2019s left: The data you\u2019ve deleted isn\u2019t lost since it was duplicated \u2013 it\u2019s still available in the \u201cCompanies\u201d table and can be used in an Investment record\u2019s formula as, e.g. $Company.company_xxx . In fact, there\u2019s a handy way to create this kind of formula. Let\u2019s create one that we\u2019ll need later. Click the header of the \u201cCompany\u201d column. In the right panel\u2019s Column tab, you\u2019ll see a section \u2018Add Referenced Columns\u2019. Click \u2018Add Column\u2019 to add the \u201ccategory_code\u201d column. A new column will be added to the table with the formula $Company.category_code . For each investment, it shows the \u201ccategory_code\u201d of the company linked to its investment record. Summarize # The powerful feature you\u2019ve been waiting for is the one that summarizes the data. Summary tables summarize each numeric column in a data table. We want to find the sum for the funding_total_usd column in the Companies table. Check that the column type is set to \u2018Numeric\u2019 and formatted with $ . To utilize this, let\u2019s add a table showing companies grouped by \u201ccategory_code\u201d. In the \u201cAdd New\u201d menu at the top left, select \u201cAdd Page\u201d. In the dialog box, select \u201cTable\u201d and \u201cCompanies\u201d, and then use the summation icon ( ) to select the \u201cGroup By\u201d columns \u2013 i.e. the columns by which to summarize. If you don\u2019t select any columns, you\u2019ll just get a single row of totals. If you summarize by \u201ccategory_code\u201d, you\u2019ll get a row for each distinct value of \u201ccategory_code\u201d. Let\u2019s do that and then click \u201cAdd Page\u201d. This is similar to Excel\u2019s pivot tables. Each row represents the group of records from the source table (\u201cCompanies\u201d) that have a particular value of \u201ccategory_code\u201d. There is a reminder of that in the table\u2019s title (\u201cCOMPANIES [by category_code]\u201d). Such summary tables can (and should!) use formulas. The columns you choose when creating the table are the identifiers of the groups. All other columns are formula columns \u2013 they are calculated. In formulas, the group of source records summarized by one row is available as the value \u201c$group\u201d. For example, you\u2019ll see a column created automatically called \u201ccount\u201d. If you hit \u201cEnter\u201d, you\u2019ll see the formula in it \u2013 len($group) \u2013 that\u2019s just the number of records in that group of records, i.e. the number of companies in that category. For numeric columns in the source table, summary tables automatically get a same-named numeric column containing a sum, with a formula such as SUM($group.funding_total_usd) . A note for Python fans $group is a special Python object. It\u2019s an iterable collection of records. Using an attribute like $group.A is a shorthand for the list of values in the A column of all the records in the group, i.e. it\u2019s roughly equivalent to [r.A for r in $group] . Sometimes, adding the values doesn\u2019t make sense. E.g. the sum of \u201cfounded_year\u201d is meaningless. It\u2019s best to delete this and any other column we don\u2019t need, which leaves us with the \u201cfunding_total_usd\u201d column. Since this column contains large numbers, it\u2019s a good time to look at the \u201cNumber Format\u201d section of its configuration, and click , (or perhaps $ ) to format numbers to be more readable. Let\u2019s add a second summary table. Select \u201cAdd New\u201d again to \u201cAdd Widget to Page\u201d. To get a summary by year, select the \u201cInvestments\u201d table under \u201cSelect Data\u201d, and again use its sum symbol (\u2211) to select the column by which to summarize: \u201cfunded_year\u201d and then click \u201cAdd to page\u201d. This produces a second summary table that shows a record for each year, with each representing a group of \u201cInvestments\u201d rows for that year. The most useful column is \u201craised_amount_usd\u201d, adding all investments made in that year. Let\u2019s delete the unneeded columns. You\u2019ll notice pink values in \u201craised_amount_usd\u201d. That\u2019s because Grist guesses the column type to be an integer.The pink sums are instances where the numbers exceed Javascript\u2019s ability to handle large integers. To correct for this, the type of the column should be switched to \u201cNumeric\u201d (which trades off precision for the ability to represent very large and very small numbers). Change the type to \u201cNumeric\u201c under \u201cColumn options\u201d. This is again a good time to pick a friendlier number format for the column, and to make it wider to fit the longer numbers. Chart, graph, plot # You can make a chart of any data. To this page, we want to add a graphic version of each summary table. Select the \u201cAdd New\u201d button again, pick \u201cAdd Widget to Page\u201d, select \u201cChart\u201d as the widget, and the same table (Companies) and summary column (category_code) as before. Then click \u201cAdd to Page\u201d. For a chart, you\u2019ll always follow up by customizing it. Open the right panel, and select \u201cChart\u201d tab / \u201cWidget\u201d subtab. For this first chart, under \u201cChart type\u201d, select \u201cPie Chart\u201d. To construct this chart, first select a label, and then select a series to summarize in the pie chart. Since we want the chart to show \u201ccategory_code\u201d as labels, select this series from the \u201cLabel\u201d dropdown. We want to use \u201cfunding_total_usd\u201d as values, so this should be listed at the top of the \u201cseries\u201d list in the configuration panel. As you move your mouse over the items in that list, use the double vertical bars that show up to drag and drop a series at the top of the list. Alternatively, you can hide the other series from the list by clicking the trash icon. Now add a chart showing a trend by year. Add another \u201cWidget to page\u201d, select \u201cChart\u201d under \u201cWidget\u201d, select \u201cInvestments\u201d under \u201cSelect Data\u201d, click summation ( ) to group by \u201cfunded_year\u201d, and click \u201cAdd to Page\u201d. To customize this chart, stick with the chart type \u201cBar Chart\u201d. From the \u201cX-Axis\u201d dropdown, select the column to use for the X (horizontal) axis values. Under \u2018Series\u2019 select a second (and possible additional) column to be values for the Y (vertical) axis. You can rearrange the sections on the screen into a configuration you\u2019d like to see for a dashboard. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. Once you\u2019re finished, rename the page by hovering over the page name then clicking the three-dot icon to open the menu. Select \u201cRename\u201d and rename to \u201cOverview\u201d. Dynamic charts # If you\u2019ve read our other tutorials on linking data, this will come naturally. Charts are simply a different way to show data, and they can be linked in the same way as tables. For our example, we\u2019ll add a new page with a summary table: select widget \u201cTable\u201d, data \u201cInvestments\u201d, group by \u201cfunded_year\u201d, click \u201cAdd Page\u201d. Let\u2019s rename this new page \u201cBreakdowns\u201d. Next, add a widget to this page, selecting widget \u201cChart\u201d, data \u201cInvestments\u201d. For \u201cGroup By\u201d, we pick two columns: \u201cCompany_category_code\u201d and \u201cfunded_year\u201d. This is why we added the \u201cCompany_category_code\u201d column earlier. We can only group investment records by the category code if we have this code for each investment. The \u201cSelect By\u201d dropdown at the bottom left of the dialog box lists widgets already on the screen that can control the selection of data in the chart we are adding. In \u201cSelect By\u201d, choose \u201cINVESTMENTS [by funded_year]\u201d, and click \u201cAdd to Page\u201d. Note: If you need to make changes to a widget you already added, such as change its type, \u201cGroup By\u201d, or \u201cSelect By\u201d settings, you can always do so from the \u201cData\u201d subtab in the widget settings, using the \u201cEdit Data Selection\u201d button. We want to be able to select a year, and then show a pie chart for that year that displays the total for each category code. The \u201cSelect By\u201d option we chose ensures that only the selected year\u2019s data is used. All that\u2019s left is to change the chart type to \u201cPie Chart\u201d, and set \u201cLabel\u201d to \u201cCompany_category_code\u201d and \u201cSeries\u201d to \u201craised_amount_usd\u201d. Note: Graphs need more screenspace, so our small screenshots will look better if we close the side panels by clicking on the opener icons ( , ). Let\u2019s also sort the table by \u201cfunded_year\u201d. As far as sorting, the highlighted button above the table reminds you that sort settings aren\u2019t saved automatically. Click the green button and select \u201cSave\u201d to do that. What\u2019s the result? We can click through the years (or use arrow keys), and see the distribution by category change. Note: If clicking through the years does not affect the chart, the chart must not be linked. You can check and correct it by using the \u201cthree dots\u201d menu on top right of the chart, clicking \u201cData selection\u201d, and ensuring that \u201cSelect By\u201d dropdown is showing \u201cINVESTMENTS [by funded_year]\u201d. To complete the example, we will add two more sections to this \u201cBreakdowns\u201d page. One will be a table listing company categories, and linked to it will be a chart showing the amount of investment in that category over the years. To add the table of categories, use \u201cAdd Widget to Page\u201d, and select \u201cTable\u201d widget, \u201cInvestments\u201d data, grouped by \u201cCompany_category_code\u201d. The \u201cfunded_year\u201d column in the resulting table is meaningless, and should be deleted. For the last step, we add another chart. We need to remember to group by both \u201cCompany_category_code\u201d \u201cfunded_year\u201d, and to set a suitable \u201cSelect By\u201d widget for it. Since there are two tables on this page, you have a choice of which one will drive the data in this chart. In this case, pick the widget that we just added: \u201cINVESTMENTS [by Company_category_code]\u201d. As in the previous section, we configure the chart by selecting \u201cChart Type\u201d as \u201cBar Chart\u201d, and in the \u201cX-Axis\u201d dropdown, selecting \u201cfunded_year\u201d and under \u201cSeries\u201d, selecting \u201craised_amount_usd\u201d and hiding the rest. We can now click through the categories, and see the history of investment into each one. Next steps # If you\u2019re unfamiliar with how we created the \u201cCompany Details\u201d page that\u2019s present in the example, visit one of these earlier tutorials to learn how: \u2018How to build a Lightweight CRM\u2019, or \u2018Managing your Business in Grist\u2019. That\u2019s it! Now go analyze some data! Download crunchbase_companies_ny.csv and crunchbase_investments_ny.csv . The sample data includes only the \u201ccompanies\u201d and \u201cinvestments\u201d data, and includes only New York companies to keep it smaller and faster. The dataset comes from Kaggle . \u21a9 Such duplication is commonly seen in spreadsheets. Data in this form is called \u201cdenormalized\u201d. \u21a9 If you don\u2019t have a single identifying column, you can construct one with a formula. \u21a9","title":"Analyze and visualize"},{"location":"investment-research/#how-to-analyze-and-visualize-data","text":"Grist offers several powerful ways to analyze and visualize data. In this tutorial, you\u2019ll learn how to: Create summary tables Create and configure charts Link charts dynamically To explain these features, we\u2019ll use the sample document \u201cInvestment Research\u201d 1 which includes companies and investments in them up to 2013. Let\u2019s take a look at the sample document and then we\u2019ll talk about how to build it so that you can apply these tools to your own data.","title":""},{"location":"investment-research/#exploring-the-example","text":"Open the document \u201c Investment Research \u201d, found in Examples & Templates in your Grist home page. The first thing you\u2019ll see is \u201cOverview\u201d. This page contains two charts next to two tables. The top left has a pie chart showing the distribution of investments by category. The table next to it has the same data in tabular form. Below the pie chart is a bar graph showing the total investments raised by year. It is also accompanied by the same data in the table next to it in tabular form. All these charts and tables are examples of \u201csummary tables\u201d, which we\u2019ll describe below. The next page, \u201cBreakdowns\u201d, also contains two tables and two charts, but these are linked dynamically and offer much more detailed insight into the data. On the top left is a table showing the total funding by year (the same table as we saw on the previous page). This table serves as a driver for the chart next to it. When you click on a year in the table, the pie chart updates to show the distribution of investments in that year. Similarly, the bottom table shows investments by category. When you click on any category, the line chart next to it updates to show the history of funding in that category over the years. Note how powerful this is, and how much insight you can gain from it. For instance, you can see that Advertising category has been getting a lot of investment in NY since 2007, but was overtaken by E-commerce in 2013, while the Fashion category had a major spike in 2011. On the next page, \u201cCompany Details\u201d, we get to see the granular data of this dataset. Here, we see a list of companies and the categories they fall into. Each company shown has a link pointing to its listing on the Crunchbase website. Selecting a company shows a card with its details, as well as a list of all the investments it has received. This is where we begin to see the power of Grist. The original dataset is a flat spreadsheet of companies, and an even bigger spreadsheet of investments. By displaying the data graphically, the data comes alive, making it powerful and useful.","title":"Exploring the example"},{"location":"investment-research/#how-can-i-make-this","text":"With Grist, presenting your own data in graphic form is a few easy steps away. Let\u2019s begin with the first step.","title":""},{"location":"investment-research/#get-the-data","text":"Let\u2019s import the raw data. We\u2019ll import two CSV files, where each will become its own table. To follow along, save the files from crunchbase_companies_ny.csv and crunchbase_investments_ny.csv to your computer first. Then, create a Grist document by importing the first file from the home page. Next, import the second table using the \u201cAdd New\u201d button and the \u201cImport from file\u201d option. In the import dialog box, finish by clicking \u201cImport\u201d on the bottom left. The tables you\u2019ve imported will be named \u201ccrunchbase_companies_ny\u201d and \u201ccrunchbase_investments_ny\u201d. Click the name at the top of the table to open the dialogue box and rename each of the tables to \u201cCompanies\u201d and \u201cInvestments\u201d.","title":"Get the data"},{"location":"investment-research/#make-it-relational","text":"The power of Grist comes from giving structure to the data. Take a look at the \u201cInvestments\u201d table. Sort by the first column and you\u2019ll notice how much repetition there is: each row contains the complete company info, which both duplicates the data in the \u201cCompanies\u201d table, and is repeated multiple times when multiple investments apply to the same company. 2 The reality is that each investment applies to a single company. Each investment row only needs to contain a reference to a company, and the data specific to that investment. To make it so, find a column that identifies a company uniquely. In this dataset, the first column, \u201ccompany_permalink\u201d, does it best 3 . Click on the arrow in the column header and click \u201cColumn Options\u201d. Click on the arrow beside \u201cText\u201d under the \u201cColumn Type\u201d in the dialogue box at the right of the screen and select \u201cReference\u201d from the list. Grist will automatically suggest to make it a \u201cReference\u201d to the \u201cCompanies\u201d table, and to show the referenced company\u2019s \u201cpermalink\u201d. Click \u201cApply\u201d to save this conversion. Let\u2019s also rename this column to \u201cCompany\u201d. In Grist, duplicated data is not needed and we recommend removing it. Using the Option-Minus (Mac) or Alt-Minus (Windows) shortcut is a quick way to remove columns. After removing the columns from \u201ccompany_name\u201d to \u201ccompany_city\u201d, here\u2019s what\u2019s left: The data you\u2019ve deleted isn\u2019t lost since it was duplicated \u2013 it\u2019s still available in the \u201cCompanies\u201d table and can be used in an Investment record\u2019s formula as, e.g. $Company.company_xxx . In fact, there\u2019s a handy way to create this kind of formula. Let\u2019s create one that we\u2019ll need later. Click the header of the \u201cCompany\u201d column. In the right panel\u2019s Column tab, you\u2019ll see a section \u2018Add Referenced Columns\u2019. Click \u2018Add Column\u2019 to add the \u201ccategory_code\u201d column. A new column will be added to the table with the formula $Company.category_code . For each investment, it shows the \u201ccategory_code\u201d of the company linked to its investment record.","title":"Make it relational"},{"location":"investment-research/#summarize","text":"The powerful feature you\u2019ve been waiting for is the one that summarizes the data. Summary tables summarize each numeric column in a data table. We want to find the sum for the funding_total_usd column in the Companies table. Check that the column type is set to \u2018Numeric\u2019 and formatted with $ . To utilize this, let\u2019s add a table showing companies grouped by \u201ccategory_code\u201d. In the \u201cAdd New\u201d menu at the top left, select \u201cAdd Page\u201d. In the dialog box, select \u201cTable\u201d and \u201cCompanies\u201d, and then use the summation icon ( ) to select the \u201cGroup By\u201d columns \u2013 i.e. the columns by which to summarize. If you don\u2019t select any columns, you\u2019ll just get a single row of totals. If you summarize by \u201ccategory_code\u201d, you\u2019ll get a row for each distinct value of \u201ccategory_code\u201d. Let\u2019s do that and then click \u201cAdd Page\u201d. This is similar to Excel\u2019s pivot tables. Each row represents the group of records from the source table (\u201cCompanies\u201d) that have a particular value of \u201ccategory_code\u201d. There is a reminder of that in the table\u2019s title (\u201cCOMPANIES [by category_code]\u201d). Such summary tables can (and should!) use formulas. The columns you choose when creating the table are the identifiers of the groups. All other columns are formula columns \u2013 they are calculated. In formulas, the group of source records summarized by one row is available as the value \u201c$group\u201d. For example, you\u2019ll see a column created automatically called \u201ccount\u201d. If you hit \u201cEnter\u201d, you\u2019ll see the formula in it \u2013 len($group) \u2013 that\u2019s just the number of records in that group of records, i.e. the number of companies in that category. For numeric columns in the source table, summary tables automatically get a same-named numeric column containing a sum, with a formula such as SUM($group.funding_total_usd) . A note for Python fans $group is a special Python object. It\u2019s an iterable collection of records. Using an attribute like $group.A is a shorthand for the list of values in the A column of all the records in the group, i.e. it\u2019s roughly equivalent to [r.A for r in $group] . Sometimes, adding the values doesn\u2019t make sense. E.g. the sum of \u201cfounded_year\u201d is meaningless. It\u2019s best to delete this and any other column we don\u2019t need, which leaves us with the \u201cfunding_total_usd\u201d column. Since this column contains large numbers, it\u2019s a good time to look at the \u201cNumber Format\u201d section of its configuration, and click , (or perhaps $ ) to format numbers to be more readable. Let\u2019s add a second summary table. Select \u201cAdd New\u201d again to \u201cAdd Widget to Page\u201d. To get a summary by year, select the \u201cInvestments\u201d table under \u201cSelect Data\u201d, and again use its sum symbol (\u2211) to select the column by which to summarize: \u201cfunded_year\u201d and then click \u201cAdd to page\u201d. This produces a second summary table that shows a record for each year, with each representing a group of \u201cInvestments\u201d rows for that year. The most useful column is \u201craised_amount_usd\u201d, adding all investments made in that year. Let\u2019s delete the unneeded columns. You\u2019ll notice pink values in \u201craised_amount_usd\u201d. That\u2019s because Grist guesses the column type to be an integer.The pink sums are instances where the numbers exceed Javascript\u2019s ability to handle large integers. To correct for this, the type of the column should be switched to \u201cNumeric\u201d (which trades off precision for the ability to represent very large and very small numbers). Change the type to \u201cNumeric\u201c under \u201cColumn options\u201d. This is again a good time to pick a friendlier number format for the column, and to make it wider to fit the longer numbers.","title":"Summarize"},{"location":"investment-research/#chart-graph-plot","text":"You can make a chart of any data. To this page, we want to add a graphic version of each summary table. Select the \u201cAdd New\u201d button again, pick \u201cAdd Widget to Page\u201d, select \u201cChart\u201d as the widget, and the same table (Companies) and summary column (category_code) as before. Then click \u201cAdd to Page\u201d. For a chart, you\u2019ll always follow up by customizing it. Open the right panel, and select \u201cChart\u201d tab / \u201cWidget\u201d subtab. For this first chart, under \u201cChart type\u201d, select \u201cPie Chart\u201d. To construct this chart, first select a label, and then select a series to summarize in the pie chart. Since we want the chart to show \u201ccategory_code\u201d as labels, select this series from the \u201cLabel\u201d dropdown. We want to use \u201cfunding_total_usd\u201d as values, so this should be listed at the top of the \u201cseries\u201d list in the configuration panel. As you move your mouse over the items in that list, use the double vertical bars that show up to drag and drop a series at the top of the list. Alternatively, you can hide the other series from the list by clicking the trash icon. Now add a chart showing a trend by year. Add another \u201cWidget to page\u201d, select \u201cChart\u201d under \u201cWidget\u201d, select \u201cInvestments\u201d under \u201cSelect Data\u201d, click summation ( ) to group by \u201cfunded_year\u201d, and click \u201cAdd to Page\u201d. To customize this chart, stick with the chart type \u201cBar Chart\u201d. From the \u201cX-Axis\u201d dropdown, select the column to use for the X (horizontal) axis values. Under \u2018Series\u2019 select a second (and possible additional) column to be values for the Y (vertical) axis. You can rearrange the sections on the screen into a configuration you\u2019d like to see for a dashboard. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. Once you\u2019re finished, rename the page by hovering over the page name then clicking the three-dot icon to open the menu. Select \u201cRename\u201d and rename to \u201cOverview\u201d.","title":"Chart, graph, plot"},{"location":"investment-research/#dynamic-charts","text":"If you\u2019ve read our other tutorials on linking data, this will come naturally. Charts are simply a different way to show data, and they can be linked in the same way as tables. For our example, we\u2019ll add a new page with a summary table: select widget \u201cTable\u201d, data \u201cInvestments\u201d, group by \u201cfunded_year\u201d, click \u201cAdd Page\u201d. Let\u2019s rename this new page \u201cBreakdowns\u201d. Next, add a widget to this page, selecting widget \u201cChart\u201d, data \u201cInvestments\u201d. For \u201cGroup By\u201d, we pick two columns: \u201cCompany_category_code\u201d and \u201cfunded_year\u201d. This is why we added the \u201cCompany_category_code\u201d column earlier. We can only group investment records by the category code if we have this code for each investment. The \u201cSelect By\u201d dropdown at the bottom left of the dialog box lists widgets already on the screen that can control the selection of data in the chart we are adding. In \u201cSelect By\u201d, choose \u201cINVESTMENTS [by funded_year]\u201d, and click \u201cAdd to Page\u201d. Note: If you need to make changes to a widget you already added, such as change its type, \u201cGroup By\u201d, or \u201cSelect By\u201d settings, you can always do so from the \u201cData\u201d subtab in the widget settings, using the \u201cEdit Data Selection\u201d button. We want to be able to select a year, and then show a pie chart for that year that displays the total for each category code. The \u201cSelect By\u201d option we chose ensures that only the selected year\u2019s data is used. All that\u2019s left is to change the chart type to \u201cPie Chart\u201d, and set \u201cLabel\u201d to \u201cCompany_category_code\u201d and \u201cSeries\u201d to \u201craised_amount_usd\u201d. Note: Graphs need more screenspace, so our small screenshots will look better if we close the side panels by clicking on the opener icons ( , ). Let\u2019s also sort the table by \u201cfunded_year\u201d. As far as sorting, the highlighted button above the table reminds you that sort settings aren\u2019t saved automatically. Click the green button and select \u201cSave\u201d to do that. What\u2019s the result? We can click through the years (or use arrow keys), and see the distribution by category change. Note: If clicking through the years does not affect the chart, the chart must not be linked. You can check and correct it by using the \u201cthree dots\u201d menu on top right of the chart, clicking \u201cData selection\u201d, and ensuring that \u201cSelect By\u201d dropdown is showing \u201cINVESTMENTS [by funded_year]\u201d. To complete the example, we will add two more sections to this \u201cBreakdowns\u201d page. One will be a table listing company categories, and linked to it will be a chart showing the amount of investment in that category over the years. To add the table of categories, use \u201cAdd Widget to Page\u201d, and select \u201cTable\u201d widget, \u201cInvestments\u201d data, grouped by \u201cCompany_category_code\u201d. The \u201cfunded_year\u201d column in the resulting table is meaningless, and should be deleted. For the last step, we add another chart. We need to remember to group by both \u201cCompany_category_code\u201d \u201cfunded_year\u201d, and to set a suitable \u201cSelect By\u201d widget for it. Since there are two tables on this page, you have a choice of which one will drive the data in this chart. In this case, pick the widget that we just added: \u201cINVESTMENTS [by Company_category_code]\u201d. As in the previous section, we configure the chart by selecting \u201cChart Type\u201d as \u201cBar Chart\u201d, and in the \u201cX-Axis\u201d dropdown, selecting \u201cfunded_year\u201d and under \u201cSeries\u201d, selecting \u201craised_amount_usd\u201d and hiding the rest. We can now click through the categories, and see the history of investment into each one.","title":"Dynamic charts"},{"location":"investment-research/#next-steps","text":"If you\u2019re unfamiliar with how we created the \u201cCompany Details\u201d page that\u2019s present in the example, visit one of these earlier tutorials to learn how: \u2018How to build a Lightweight CRM\u2019, or \u2018Managing your Business in Grist\u2019. That\u2019s it! Now go analyze some data! Download crunchbase_companies_ny.csv and crunchbase_investments_ny.csv . The sample data includes only the \u201ccompanies\u201d and \u201cinvestments\u201d data, and includes only New York companies to keep it smaller and faster. The dataset comes from Kaggle . \u21a9 Such duplication is commonly seen in spreadsheets. Data in this form is called \u201cdenormalized\u201d. \u21a9 If you don\u2019t have a single identifying column, you can construct one with a formula. \u21a9","title":"Next steps"},{"location":"keyboard-shortcuts/","text":"Grist Shortcuts # General # Key (Mac) Key (Windows) Description F1 , \u2318 / F1 , Ctrl + / Display shortcuts pane \u2318 Z Ctrl + Z Undo last action \u2318 \u21e7 Z , \u2303 Y Ctrl + Shift + Z , Ctrl + Y Redo last action \u2318 F Ctrl + F Find \u2318 G Ctrl + G Find next occurrence \u2318 \u21e7 G Ctrl + Shift + G Find previous occurrence Navigation # Key (Mac) Key (Windows) Description \u2193 \u2193 Move downward to next record or field \u2191 \u2191 Move upward to previous record or field \u2192 \u2192 Move right to the next field \u2190 \u2190 Move left to the previous field Tab Tab Move to the next field, saving changes if editing a value \u21e7 Tab Shift + Tab Move to the previous field, saving changes if editing a value PageDown PageDown Move down one page of records, or to next record in a card list PageUp PageUp Move up one page of records, or to previous record in a card list \u2318 \u2191 Ctrl + \u2191 Move up to the first record \u2318 \u2193 Ctrl + \u2193 Move down to the last record Home Home Move to the first field or the beginning of a row End End Move to the last field or the end of a row \u2325 \u2193 Alt + \u2193 Open next page \u2325 \u2191 Alt + \u2191 Open previous page \u2318 O Ctrl + O Activate next page widget \u2318 \u21e7 O Ctrl + Shift + O Activate previous page widget Space Space Opens a record card in a table widget Selection # Key (Mac) Key (Windows) Description \u21e7 \u2193 Shift + \u2193 Adds the element below the cursor to the selected range \u21e7 \u2191 Shift + \u2191 Adds the element above the cursor to the selected range \u21e7 \u2192 Shift + \u2192 Adds the element to the right of the cursor to the selected range \u21e7 \u2190 Shift + \u2190 Adds the element to the left of the cursor to the selected range \u2318 A Ctrl + A Selects all currently displayed cells \u2318 Shift \u2191 Ctrl + Shift + \u2191 Selects cells above the selected cell in the same column \u2318 Shift \u2193 Ctrl + Shift + \u2193 Selects cells below the selected cell in the same column \u2318 Shift \u2192 Ctrl + Shift + \u2192 Selects cells to the right of the selected cell in the same row \u2318 Shift \u2190 Ctrl + Shift + \u2190 Selects cells to the left of the selected cell in the same row \u2318 \u21e7 A Ctrl + Shift + A Copy anchor link Editing # Key (Mac) Key (Windows) Description Enter , F2 Enter , F2 Start editing the currently-selected cell Enter Enter Finish editing a cell, saving the value Escape Escape Discard changes to a cell value \u2318 D Ctrl + D Fills current selection with the contents of the top row in the selection Delete Backspace , Delete Clears the currently selected cells Enter Enter Toggles the value of checkbox cells = = When typed at the start of a cell, make this a formula column \u2318 ; Ctrl + ; Insert the current date \u2318 \u21e7 ; Ctrl + Shift + ; Insert the current date and time Data manipulation # Key (Mac) Key (Windows) Description \u2318 \u21e7 Enter Ctrl + Shift + Enter Insert a new record, before the currently selected one in an unsorted table \u2318 Enter Ctrl + Enter Insert a new record, after the currently selected one in an unsorted table \u2318 \u21e7 D Ctrl + Shift + D Duplicate the currently selected record(s) \u2318 Delete Ctrl + Backspace , Ctrl + Delete Delete the currently selected record(s) \u2325 \u21e7 = Alt + Shift + = Insert a new column, before the currently selected one \u2325 = Alt + = Insert a new column, after the currently selected one \u2303 M Ctrl + M Rename the currently selected column \u2325 \u21e7 - Alt + Shift + - Hide the currently selected column \u2325 - Alt + - Delete the currently selected columns","title":"Shortcuts"},{"location":"keyboard-shortcuts/#grist-shortcuts","text":"","title":"Grist Shortcuts"},{"location":"keyboard-shortcuts/#general","text":"Key (Mac) Key (Windows) Description F1 , \u2318 / F1 , Ctrl + / Display shortcuts pane \u2318 Z Ctrl + Z Undo last action \u2318 \u21e7 Z , \u2303 Y Ctrl + Shift + Z , Ctrl + Y Redo last action \u2318 F Ctrl + F Find \u2318 G Ctrl + G Find next occurrence \u2318 \u21e7 G Ctrl + Shift + G Find previous occurrence","title":"General"},{"location":"keyboard-shortcuts/#navigation","text":"Key (Mac) Key (Windows) Description \u2193 \u2193 Move downward to next record or field \u2191 \u2191 Move upward to previous record or field \u2192 \u2192 Move right to the next field \u2190 \u2190 Move left to the previous field Tab Tab Move to the next field, saving changes if editing a value \u21e7 Tab Shift + Tab Move to the previous field, saving changes if editing a value PageDown PageDown Move down one page of records, or to next record in a card list PageUp PageUp Move up one page of records, or to previous record in a card list \u2318 \u2191 Ctrl + \u2191 Move up to the first record \u2318 \u2193 Ctrl + \u2193 Move down to the last record Home Home Move to the first field or the beginning of a row End End Move to the last field or the end of a row \u2325 \u2193 Alt + \u2193 Open next page \u2325 \u2191 Alt + \u2191 Open previous page \u2318 O Ctrl + O Activate next page widget \u2318 \u21e7 O Ctrl + Shift + O Activate previous page widget Space Space Opens a record card in a table widget","title":"Navigation"},{"location":"keyboard-shortcuts/#selection","text":"Key (Mac) Key (Windows) Description \u21e7 \u2193 Shift + \u2193 Adds the element below the cursor to the selected range \u21e7 \u2191 Shift + \u2191 Adds the element above the cursor to the selected range \u21e7 \u2192 Shift + \u2192 Adds the element to the right of the cursor to the selected range \u21e7 \u2190 Shift + \u2190 Adds the element to the left of the cursor to the selected range \u2318 A Ctrl + A Selects all currently displayed cells \u2318 Shift \u2191 Ctrl + Shift + \u2191 Selects cells above the selected cell in the same column \u2318 Shift \u2193 Ctrl + Shift + \u2193 Selects cells below the selected cell in the same column \u2318 Shift \u2192 Ctrl + Shift + \u2192 Selects cells to the right of the selected cell in the same row \u2318 Shift \u2190 Ctrl + Shift + \u2190 Selects cells to the left of the selected cell in the same row \u2318 \u21e7 A Ctrl + Shift + A Copy anchor link","title":"Selection"},{"location":"keyboard-shortcuts/#editing","text":"Key (Mac) Key (Windows) Description Enter , F2 Enter , F2 Start editing the currently-selected cell Enter Enter Finish editing a cell, saving the value Escape Escape Discard changes to a cell value \u2318 D Ctrl + D Fills current selection with the contents of the top row in the selection Delete Backspace , Delete Clears the currently selected cells Enter Enter Toggles the value of checkbox cells = = When typed at the start of a cell, make this a formula column \u2318 ; Ctrl + ; Insert the current date \u2318 \u21e7 ; Ctrl + Shift + ; Insert the current date and time","title":"Editing"},{"location":"keyboard-shortcuts/#data-manipulation","text":"Key (Mac) Key (Windows) Description \u2318 \u21e7 Enter Ctrl + Shift + Enter Insert a new record, before the currently selected one in an unsorted table \u2318 Enter Ctrl + Enter Insert a new record, after the currently selected one in an unsorted table \u2318 \u21e7 D Ctrl + Shift + D Duplicate the currently selected record(s) \u2318 Delete Ctrl + Backspace , Ctrl + Delete Delete the currently selected record(s) \u2325 \u21e7 = Alt + Shift + = Insert a new column, before the currently selected one \u2325 = Alt + = Insert a new column, after the currently selected one \u2303 M Ctrl + M Rename the currently selected column \u2325 \u21e7 - Alt + Shift + - Hide the currently selected column \u2325 - Alt + - Delete the currently selected columns","title":"Data manipulation"},{"location":"lightweight-crm/","text":"How to create a custom CRM # Grist is as easy to use as a spreadsheet, but gives you new powers when data doesn\u2019t fit in a simple grid. A good example is keeping track of contacts and our conversations with them. For a business, this could be customers, sales leads, or job candidates. For an individual, it could be companies they have applied to in a job search. In this tutorial, we\u2019ll explain the \u201cLightweight CRM\u201d example, which you can use as a template for your own contacts, and then show how to build it from scratch. You\u2019ll learn how to: Add tables Link data Set column types Create custom layouts Exploring the example # Open the document Lightweight CRM , found in Examples & Templates in your Grist home page . You\u2019ll see the \u201cContacts\u201d page, listing contacts on the left of the screen. Click on any contact to select it. The two sections on the right show the details and the history of interactions with the selected contact. How does this compare to a spreadsheet? These screenshots show the Lightweight CRM example on the left, and a regular spreadsheet with the same data on the right. Previous Next Previous Next The difficulty is in the history of notes for a contact. In a two-dimensional grid, you have few options for where to include multiple notes. If you include them as multiple columns, it quickly makes the spreadsheet unwieldy and difficult to navigate. Grist feels more like an application, but it\u2019s still as versatile as a spreadsheet. The \u201cLightweight CRM\u201d example can be used immediately as-is (with the sample data), or as a template (just the structure without the data). Here are a few more points on using it as a CRM: To add a new contact, click the blank row at the bottom of the contacts list, then fill in the blank \u201cCONTACTS Card\u201d section that shows on the right. To add a new conversation, select a contact, then click the blank line at the end of the Interactions table. You can enter today\u2019s date using the shortcut \u2318 + ; (semicolon) (Mac) or Ctrl + ; (semicolon) (Windows). Then select the type of interaction using auto-complete, and type in your notes. You can add To-Do items for a contact: in the Interactions list, select \u201cTo-Do\u201d in the \u201cType\u201d column as a special type of interaction. Think of the associated date as the due date for this task. The Contacts table shows the list of coming up To-Do items, sorted by their due date. If you use Gmail, the handy \u201cGmail search\u201d link in the \u201cCONTACTS Card\u201d section will open a browser window with the Gmail search results for this contact\u2019s email address. You can use this example as a template for your own contacts. With the \u201cLightweight CRM\u201d example open, click the \u201cSave Copy\u201d button in in the top bar, then mark the \u201cAs Template\u201d checkbox. You\u2019ll get an empty document with the same layout, and can start filling it in with your own data. If you aren\u2019t signed in, you will need to sign in to make a copy of the example. Creating your own # The rest of this tutorial will show you how to create such a document on your own. It\u2019s a great exercise that will teach you some of the key features of Grist. To start, we\u2019ll import a file with sample contacts from the Grist home page. First, save this file to your desktop: lightweight-crm-contacts.csv . Then click the \u201cAdd New\u201d button on the top left of your Grist home page, click \u201cImport document\u201d, and select the file on your desktop. You\u2019ll see a table of contacts with sample data. Note that in Grist, columns have names. Rename this table to \u201cContacts\u201d by clicking its name in the top bar, and typing the new name. That\u2019s all you need for a simple table of contacts. You can add rows here, or add new columns to associate more data with each contact. Adding another table # For our next step, we want to be able to select a contact, and see the list of conversations with that contact. These conversations should be a new table of data. The cue is that it has a different number of rows from the table of contacts. Create the new table using the green \u201cAdd New\u201d button on the top left of your screen, and click \u201cAdd Empty Table\u201d in the menu. This table will represent interactions with our contacts, so let\u2019s rename it \u201cInteractions\u201d by clicking its default name (\u201cTable1\u201d) on top of the screen, as before. It\u2019s a good idea to give meaningful names to columns. In this case, for each interaction, we need to know which Contact it refers to, the date, type, and conversation notes. To rename a column, click its header to select the column, and click the header again to edit its name. You can hit the Tab key to continue to renaming the next column. Finally, hit the \u201c+\u201d button to the right of the last column to create one more column, and name it \u201cNotes\u201d. Linking data records # Every record in this table will belong to a particular contact. You set it up by turning the \u201cContact\u201d column into a reference to the table \u201cContacts\u201d. Using the triangle in the header of the column \u201cContact\u201d, open the menu and select \u201cColumn Options\u201d. In the right panel, use the \u201cColumn Type\u201d dropdown to select \u201cReference\u201d, then under \u201cData from table\u201d, select \u201cContacts\u201d. Each cell in this column will hold a pointer to a row in the \u201cContacts\u201d table 1 . While it refers to an entire row, it\u2019s useful to see some particular identifier of that row, so under \u201cShow column\u201d, select \u201cCompany\u201d. You\u2019ll see this in action shortly. Setting other types # In Grist, every column has a type. Often, the default of Text or Numeric is correct. For our \u201cDate\u201d column, a better type is Date. Click any cell in the \u201cDate\u201d column, and in the right panel, click into the \u201cColumn Type\u201d dropdown and select \u201cDate\u201d. If you\u2019d like, you can also choose a different date format right below the type. Now, if you click on a cell in the \u201cDate\u201d column and hit Enter, you have a convenient date picker. Another useful column type for us is \u201cChoice\u201d. Our interactions will be either \u201cPhone\u201d, \u201cEmail\u201d, or \u201cIn-person\u201d, and it\u2019s useful to list these options. Click into the \u201cType\u201d column, and in the right panel, set \u201cColumn Type\u201d to \u201cChoice\u201d. You\u2019ll see \u201cChoice Values\u201d textbox below. Click it, and enter your choices there, one per line: \u201cPhone\u201d, \u201cEmail\u201d, \u201cIn-person\u201d. Now, if you click on a cell in the \u201cType\u201d column and hit Enter, you can now choose from among the choices you set, or start typing and use auto-complete. Linking tables visually # The next step is to link the two tables visually. Open the \u201cContacts\u201d page, click the \u201cAdd New\u201d button on top of the left panel, then \u201cAdd Widget to Page\u201d. Select widget \u201cTable\u201d and data \u201cInteractions\u201d. In the \u201cSelect By\u201d dropdown at the bottom of the dialog, select \u201cCONTACTS\u201d. This means that choosing a contact will display only the interactions with that contact. Click \u201cAdd to Page\u201d to finish. Next, let\u2019s select a contact in the table on the left (let\u2019s use \u201cDouglas LLC\u201d in the fourth row) and add some notes for it. Type in a date (hint: the shortcut \u2318 + ; (semicolon) on Mac or Ctrl + ; (semicolon) on Windows inserts today\u2019s date), select a type, and enter a note. As soon as that row is created, the \u201cContact\u201d column is automatically filled with \u201cDouglas LLC\u201d, thanks to the sections being linked. The note we added is shown only when \u201cDouglas LLC\u201d is selected. We can add more notes for \u201cDouglas LLC\u201d, or add notes for any of the other contacts. We can now hide the \u201cContact\u201d column in the \u201cInteractions\u201d table: using the menu in the column\u2019s header, select \u201cHide Column\u201d. Because the tables are linked, we already see who the notes are for. For longer notes to be convenient, resize the \u201cNotes\u201d column by dragging the right edge of its header. To wrap long notes, open the Column Options, and click the line-wrapping icon. Customizing layout # Once you have multiple tables on one screen, the layout of the screen may become an issue. Having many columns in the Contacts table may no longer be convenient. It\u2019s better to lay it out like a custom application: select a contact from a list on the left and see that contact\u2019s details and interactions. This can be done by using \u201cAdd Widget to Page\u201d again. This time, we\u2019ll select the widget \u201cCard\u201d for the table \u201cContacts\u201d, and for \u201cSelect By\u201d will again use \u201cCONTACTS\u201d. You can move the resulting sections around to create a convenient layout. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. You can also resize sections by moving the mouse between them to find a dotted line. Drag this line to resize. Note how the same personal data is now shown in two places on the screen. These are not copies of data, but different presentations of the same data. Changing the data in one place will change it in the other. Customizing fields # At this point, we may do some cleanup: hide unneeded columns in the main \u201cContacts\u201d table and rearrange fields in the Card widget. A quick way to hide columns is using the right panel. Using the three-dot menu on the top right of the \u201cContacts\u201d table, select \u201cWidget options\u201d. In the panel that opens, find a list of \u201cVisible columns\u201d. Move the mouse over each column to reveal the \u201ceye\u201d icon. Click it to hide all columns except \u201cCompany\u201d. To customize the Card widget, click it. The right panel will show the relevant options. You can select a different Theme, e.g. \u201cCompact\u201d. To rearrange fields, click \u201cEdit Layout\u201d in the right panel. You can now drag-and-drop fields in the card, resize them, or remove them. Click \u201cSave\u201d once you are done. In a few short steps, we have gone from a clunky, unwieldy spreadsheet to a concise, elegant record of your interactions in a simple, effective custom application. To-Do Tasks for Contacts # The \u201cLightweight CRM\u201d example has another trick up its sleeve. The \u201cType\u201d column in the interactions table has an extra choice, \u201cTo-Do\u201d. After you talk to a contact, you can add an extra note about what you need to do for the next conversation, and the date when it\u2019s due. The \u201cContacts\u201d table makes these To-Do items visible, and sortable by due date. This way you can see at a glance what\u2019s coming up next. If you are interested in the details of setting it up, expand the section below. For your first introduction to Grist, you are welcome to skip it. > Setting up To-Do tasks # To set up To-Do items as in the example, select Column Options for the \u201cType\u201d column in the Interactions table, and add another choice (\u201cTo-Do\u201d) to the list of choices: Let\u2019s pick our contact \u201cDouglas LLC\u201d and add a To-Do item: In the \u201cContacts\u201d table, add two new columns: Rename them to \u201cDue\u201d and \u201cTo-Do Items\u201d. Both columns are calculated using formulas. Grist has great support for formulas, allowing full Python syntax and many Excel functions. In Grist, a formula always applies to the entire column of data. To enter a formula, click on a cell in the \u201cDue\u201d column, and hit \u201c=\u201d key: In this formula, we want to look up all Interactions for the current Contact whose Type is \u201cTo-Do\u201d, then select the one with the earliest Date. Using Python syntax, the formula is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Paste it in, or type in. When typing in multi-line formulas, use Shift+Enter to add new lines, and Enter to save. It\u2019s also a good time to change the column type to \u201cDate\u201d. Open Column Options, and select \u201cDate\u201d for the type. You can choose the Date Format directly below the type. For the \u201cTo-Do Items\u201d, enter a formula similarly. In case of multiple To-Do items, this formula will concatenate them, separated by line breaks. Click into the \u201cTo-Do Items\u201d column, hit \u201c=\u201d to start typing the formula, and enter items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return \"\\n\".join(items.Notes) Now the To-Do item we entered earlier is visible in the main Contacts table. Note that the values in these columns are read-only, since they are calculated. To change the due date, find the To-Do item in the Interactions table where you created it. Sorting tables # We\u2019ll want contacts with To-Do items to show up first, in order of the Due date. Click the triangle in the header of the \u201cDue\u201d column, and select \u201cSort A-Z\u201d. By default, sorting settings are not saved. The highlighted green button in the top right of the \u201cContacts\u201d table reminds us of that. To keep this sort order when you reopen the document, save it by clicking that green button and selecting \u201cSave\u201d. You can also save by clicking the green check mark, to the right of the filter icon. Other features # Grist has more great features, some of which are used in the \u201cLightweight CRM\u201d example document. To read more about those, follow the links to their documentation. Any text column may be shown as a hyperlink . Lightweight CRM example uses it twice: for the \u201cwebsite\u201d field, and for a formula-constructed hyperlink to a Gmail search page for the given contact\u2019s email. The latter is handy if you use Gmail. Grist supports attachments . In the example, there is an \u201cAttachments\u201d field for each contact that may be used to store an image of a business card, for example. In an actual business, you\u2019ll need more. Specialized CRM products have tons of features. Their problem is complexity: trying to satisfy the needs of many different clients makes for a complicated product to use. The beauty of Grist is that you can start simple and add only the level of complexity you need, and nothing more. Other tutorials show how to model more complex data, analyze and chart data, and more. In the database world, this kind of reference or pointer is known as a \u201cforeign key\u201d. \u21a9","title":"Create your own CRM"},{"location":"lightweight-crm/#how-to-create-a-custom-crm","text":"Grist is as easy to use as a spreadsheet, but gives you new powers when data doesn\u2019t fit in a simple grid. A good example is keeping track of contacts and our conversations with them. For a business, this could be customers, sales leads, or job candidates. For an individual, it could be companies they have applied to in a job search. In this tutorial, we\u2019ll explain the \u201cLightweight CRM\u201d example, which you can use as a template for your own contacts, and then show how to build it from scratch. You\u2019ll learn how to: Add tables Link data Set column types Create custom layouts","title":"Intro"},{"location":"lightweight-crm/#exploring-the-example","text":"Open the document Lightweight CRM , found in Examples & Templates in your Grist home page . You\u2019ll see the \u201cContacts\u201d page, listing contacts on the left of the screen. Click on any contact to select it. The two sections on the right show the details and the history of interactions with the selected contact. How does this compare to a spreadsheet? These screenshots show the Lightweight CRM example on the left, and a regular spreadsheet with the same data on the right. Previous Next Previous Next The difficulty is in the history of notes for a contact. In a two-dimensional grid, you have few options for where to include multiple notes. If you include them as multiple columns, it quickly makes the spreadsheet unwieldy and difficult to navigate. Grist feels more like an application, but it\u2019s still as versatile as a spreadsheet. The \u201cLightweight CRM\u201d example can be used immediately as-is (with the sample data), or as a template (just the structure without the data). Here are a few more points on using it as a CRM: To add a new contact, click the blank row at the bottom of the contacts list, then fill in the blank \u201cCONTACTS Card\u201d section that shows on the right. To add a new conversation, select a contact, then click the blank line at the end of the Interactions table. You can enter today\u2019s date using the shortcut \u2318 + ; (semicolon) (Mac) or Ctrl + ; (semicolon) (Windows). Then select the type of interaction using auto-complete, and type in your notes. You can add To-Do items for a contact: in the Interactions list, select \u201cTo-Do\u201d in the \u201cType\u201d column as a special type of interaction. Think of the associated date as the due date for this task. The Contacts table shows the list of coming up To-Do items, sorted by their due date. If you use Gmail, the handy \u201cGmail search\u201d link in the \u201cCONTACTS Card\u201d section will open a browser window with the Gmail search results for this contact\u2019s email address. You can use this example as a template for your own contacts. With the \u201cLightweight CRM\u201d example open, click the \u201cSave Copy\u201d button in in the top bar, then mark the \u201cAs Template\u201d checkbox. You\u2019ll get an empty document with the same layout, and can start filling it in with your own data. If you aren\u2019t signed in, you will need to sign in to make a copy of the example.","title":"Exploring the example"},{"location":"lightweight-crm/#creating-your-own","text":"The rest of this tutorial will show you how to create such a document on your own. It\u2019s a great exercise that will teach you some of the key features of Grist. To start, we\u2019ll import a file with sample contacts from the Grist home page. First, save this file to your desktop: lightweight-crm-contacts.csv . Then click the \u201cAdd New\u201d button on the top left of your Grist home page, click \u201cImport document\u201d, and select the file on your desktop. You\u2019ll see a table of contacts with sample data. Note that in Grist, columns have names. Rename this table to \u201cContacts\u201d by clicking its name in the top bar, and typing the new name. That\u2019s all you need for a simple table of contacts. You can add rows here, or add new columns to associate more data with each contact.","title":"Creating your own"},{"location":"lightweight-crm/#adding-another-table","text":"For our next step, we want to be able to select a contact, and see the list of conversations with that contact. These conversations should be a new table of data. The cue is that it has a different number of rows from the table of contacts. Create the new table using the green \u201cAdd New\u201d button on the top left of your screen, and click \u201cAdd Empty Table\u201d in the menu. This table will represent interactions with our contacts, so let\u2019s rename it \u201cInteractions\u201d by clicking its default name (\u201cTable1\u201d) on top of the screen, as before. It\u2019s a good idea to give meaningful names to columns. In this case, for each interaction, we need to know which Contact it refers to, the date, type, and conversation notes. To rename a column, click its header to select the column, and click the header again to edit its name. You can hit the Tab key to continue to renaming the next column. Finally, hit the \u201c+\u201d button to the right of the last column to create one more column, and name it \u201cNotes\u201d.","title":"Adding another table"},{"location":"lightweight-crm/#linking-data-records","text":"Every record in this table will belong to a particular contact. You set it up by turning the \u201cContact\u201d column into a reference to the table \u201cContacts\u201d. Using the triangle in the header of the column \u201cContact\u201d, open the menu and select \u201cColumn Options\u201d. In the right panel, use the \u201cColumn Type\u201d dropdown to select \u201cReference\u201d, then under \u201cData from table\u201d, select \u201cContacts\u201d. Each cell in this column will hold a pointer to a row in the \u201cContacts\u201d table 1 . While it refers to an entire row, it\u2019s useful to see some particular identifier of that row, so under \u201cShow column\u201d, select \u201cCompany\u201d. You\u2019ll see this in action shortly.","title":"Linking data records"},{"location":"lightweight-crm/#setting-other-types","text":"In Grist, every column has a type. Often, the default of Text or Numeric is correct. For our \u201cDate\u201d column, a better type is Date. Click any cell in the \u201cDate\u201d column, and in the right panel, click into the \u201cColumn Type\u201d dropdown and select \u201cDate\u201d. If you\u2019d like, you can also choose a different date format right below the type. Now, if you click on a cell in the \u201cDate\u201d column and hit Enter, you have a convenient date picker. Another useful column type for us is \u201cChoice\u201d. Our interactions will be either \u201cPhone\u201d, \u201cEmail\u201d, or \u201cIn-person\u201d, and it\u2019s useful to list these options. Click into the \u201cType\u201d column, and in the right panel, set \u201cColumn Type\u201d to \u201cChoice\u201d. You\u2019ll see \u201cChoice Values\u201d textbox below. Click it, and enter your choices there, one per line: \u201cPhone\u201d, \u201cEmail\u201d, \u201cIn-person\u201d. Now, if you click on a cell in the \u201cType\u201d column and hit Enter, you can now choose from among the choices you set, or start typing and use auto-complete.","title":"Setting other types"},{"location":"lightweight-crm/#linking-tables-visually","text":"The next step is to link the two tables visually. Open the \u201cContacts\u201d page, click the \u201cAdd New\u201d button on top of the left panel, then \u201cAdd Widget to Page\u201d. Select widget \u201cTable\u201d and data \u201cInteractions\u201d. In the \u201cSelect By\u201d dropdown at the bottom of the dialog, select \u201cCONTACTS\u201d. This means that choosing a contact will display only the interactions with that contact. Click \u201cAdd to Page\u201d to finish. Next, let\u2019s select a contact in the table on the left (let\u2019s use \u201cDouglas LLC\u201d in the fourth row) and add some notes for it. Type in a date (hint: the shortcut \u2318 + ; (semicolon) on Mac or Ctrl + ; (semicolon) on Windows inserts today\u2019s date), select a type, and enter a note. As soon as that row is created, the \u201cContact\u201d column is automatically filled with \u201cDouglas LLC\u201d, thanks to the sections being linked. The note we added is shown only when \u201cDouglas LLC\u201d is selected. We can add more notes for \u201cDouglas LLC\u201d, or add notes for any of the other contacts. We can now hide the \u201cContact\u201d column in the \u201cInteractions\u201d table: using the menu in the column\u2019s header, select \u201cHide Column\u201d. Because the tables are linked, we already see who the notes are for. For longer notes to be convenient, resize the \u201cNotes\u201d column by dragging the right edge of its header. To wrap long notes, open the Column Options, and click the line-wrapping icon.","title":"Linking tables visually"},{"location":"lightweight-crm/#customizing-layout","text":"Once you have multiple tables on one screen, the layout of the screen may become an issue. Having many columns in the Contacts table may no longer be convenient. It\u2019s better to lay it out like a custom application: select a contact from a list on the left and see that contact\u2019s details and interactions. This can be done by using \u201cAdd Widget to Page\u201d again. This time, we\u2019ll select the widget \u201cCard\u201d for the table \u201cContacts\u201d, and for \u201cSelect By\u201d will again use \u201cCONTACTS\u201d. You can move the resulting sections around to create a convenient layout. Move your mouse to the top left of each section until you see a \u201cdrag handle\u201d icon. Use that icon to drag each section into the desired spot relative to other sections. You can also resize sections by moving the mouse between them to find a dotted line. Drag this line to resize. Note how the same personal data is now shown in two places on the screen. These are not copies of data, but different presentations of the same data. Changing the data in one place will change it in the other.","title":"Customizing layout"},{"location":"lightweight-crm/#customizing-fields","text":"At this point, we may do some cleanup: hide unneeded columns in the main \u201cContacts\u201d table and rearrange fields in the Card widget. A quick way to hide columns is using the right panel. Using the three-dot menu on the top right of the \u201cContacts\u201d table, select \u201cWidget options\u201d. In the panel that opens, find a list of \u201cVisible columns\u201d. Move the mouse over each column to reveal the \u201ceye\u201d icon. Click it to hide all columns except \u201cCompany\u201d. To customize the Card widget, click it. The right panel will show the relevant options. You can select a different Theme, e.g. \u201cCompact\u201d. To rearrange fields, click \u201cEdit Layout\u201d in the right panel. You can now drag-and-drop fields in the card, resize them, or remove them. Click \u201cSave\u201d once you are done. In a few short steps, we have gone from a clunky, unwieldy spreadsheet to a concise, elegant record of your interactions in a simple, effective custom application.","title":"Customizing fields"},{"location":"lightweight-crm/#to-do-tasks-for-contacts","text":"The \u201cLightweight CRM\u201d example has another trick up its sleeve. The \u201cType\u201d column in the interactions table has an extra choice, \u201cTo-Do\u201d. After you talk to a contact, you can add an extra note about what you need to do for the next conversation, and the date when it\u2019s due. The \u201cContacts\u201d table makes these To-Do items visible, and sortable by due date. This way you can see at a glance what\u2019s coming up next. If you are interested in the details of setting it up, expand the section below. For your first introduction to Grist, you are welcome to skip it.","title":"To-Do Tasks for Contacts"},{"location":"lightweight-crm/#setting-up-to-do-tasks","text":"To set up To-Do items as in the example, select Column Options for the \u201cType\u201d column in the Interactions table, and add another choice (\u201cTo-Do\u201d) to the list of choices: Let\u2019s pick our contact \u201cDouglas LLC\u201d and add a To-Do item: In the \u201cContacts\u201d table, add two new columns: Rename them to \u201cDue\u201d and \u201cTo-Do Items\u201d. Both columns are calculated using formulas. Grist has great support for formulas, allowing full Python syntax and many Excel functions. In Grist, a formula always applies to the entire column of data. To enter a formula, click on a cell in the \u201cDue\u201d column, and hit \u201c=\u201d key: In this formula, we want to look up all Interactions for the current Contact whose Type is \u201cTo-Do\u201d, then select the one with the earliest Date. Using Python syntax, the formula is: items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return min(items.Date) if items else None Paste it in, or type in. When typing in multi-line formulas, use Shift+Enter to add new lines, and Enter to save. It\u2019s also a good time to change the column type to \u201cDate\u201d. Open Column Options, and select \u201cDate\u201d for the type. You can choose the Date Format directly below the type. For the \u201cTo-Do Items\u201d, enter a formula similarly. In case of multiple To-Do items, this formula will concatenate them, separated by line breaks. Click into the \u201cTo-Do Items\u201d column, hit \u201c=\u201d to start typing the formula, and enter items = Interactions.lookupRecords(Contact=$id, Type=\"To-Do\") return \"\\n\".join(items.Notes) Now the To-Do item we entered earlier is visible in the main Contacts table. Note that the values in these columns are read-only, since they are calculated. To change the due date, find the To-Do item in the Interactions table where you created it.","title":""},{"location":"lightweight-crm/#sorting-tables","text":"We\u2019ll want contacts with To-Do items to show up first, in order of the Due date. Click the triangle in the header of the \u201cDue\u201d column, and select \u201cSort A-Z\u201d. By default, sorting settings are not saved. The highlighted green button in the top right of the \u201cContacts\u201d table reminds us of that. To keep this sort order when you reopen the document, save it by clicking that green button and selecting \u201cSave\u201d. You can also save by clicking the green check mark, to the right of the filter icon.","title":""},{"location":"lightweight-crm/#other-features","text":"Grist has more great features, some of which are used in the \u201cLightweight CRM\u201d example document. To read more about those, follow the links to their documentation. Any text column may be shown as a hyperlink . Lightweight CRM example uses it twice: for the \u201cwebsite\u201d field, and for a formula-constructed hyperlink to a Gmail search page for the given contact\u2019s email. The latter is handy if you use Gmail. Grist supports attachments . In the example, there is an \u201cAttachments\u201d field for each contact that may be used to store an image of a business card, for example. In an actual business, you\u2019ll need more. Specialized CRM products have tons of features. Their problem is complexity: trying to satisfy the needs of many different clients makes for a complicated product to use. The beauty of Grist is that you can start simple and add only the level of complexity you need, and nothing more. Other tutorials show how to model more complex data, analyze and chart data, and more. In the database world, this kind of reference or pointer is known as a \u201cforeign key\u201d. \u21a9","title":"Other features"},{"location":"limits/","text":"Limits # To help you assess whether Grist will work for a use-case you have in mind, here is a list of limitations that apply to its operation. Number of documents # On all plans, the number of documents is not limited. To prevent accidental abuse of the system by automation tools, team sites may be limited to 1,000 documents. If you encounter such a limit for legitimate use, please contact support to increase it. Older free plans had a limit of ten documents. Learn more about legacy limits . Number of collaborators # For team sites on all plans, there is no limit on the number of team members that may be added to the site. For paid plans, the number of team members determines the price. See our pricing page for details. Team members added to your team site may inherit access to workspaces or documents within that organization. Learn more about team sharing . On both personal and team sites, each document may be shared with up to 2 free guests who do not affect the plan price even on paid plans. Number of tables per document # There is a limit of 500 tables allowed per document. This is a soft limit. If you find yourself with a large number of tables, consider merging ones that have the same structure. For example, if you have a table for each product type, consider using a single table with the product type as an extra column. Rows per document # On the Free plan, documents have a limit of 5,000 rows. The limit for Pro and Business plans is 100,000 and 150,000 rows, respectively. Documents are also subject to data size limits, as described below. Data size # There is a hard limit to a document\u2019s total data size, determined as the row limit multiplied by 2KB. This means that documents on the Free plan have a data size limit of 10MB, with Pro and Business plan documents having limits of 200MB and 300MB, respectively. This value corresponds approximately to the size of the data in CSV format. You can see a document\u2019s current data size on the \u2018Raw Data\u2019 page . For memory and performance reasons, there\u2019s a recommended data size limit of 20MB. Documents beyond 20MB may slow down or run into memory limits depending on their complexity. As an example, a document with 100,000 rows and 24 numeric columns would reach this recommended limit. To help optimize formulas on large documents, you can use the built-in formula timer . Attachments are counted separately. Attachments plus data in a single document are limited to 1GB on all plans. Uploads # Uploads are limited to 50MB, both for attachments and to import data. Note that an import within this limit may result in a document that exceeds the document size limit, in which case the upload is still likely to fail. API limits # Free plans are limited to 5,000 API calls per document per day. Pro and Business plans raise the limit to 40,000 and 60,000 calls per document per day, respectively. Free plans may be rate limited to 5 API requests per second per document. The team plan does not impose such a rate limit. Separately, there is a concurrency limit of 10 for all plans: if 10 authorized API requests are currently being processed for a particular document, any other API requests will be rejected (with HTTP status code 429) until at least one of the original requests completes. A client that waits for one request to complete before sending the next would not hit this limit (assuming it is the sole client accessing the document). The size of the body of any individual API request is limited to 1MB. In particular, this means that requests adding or updating multiple records may need to be split into batches that fit within this limit. Document availability # From time to time, during upgrades and operational transitions, individual Grist documents may become inaccessible for a period of some seconds. Please bear this in mind when using Grist\u2019s API. Legacy limits # Older free personal plans have the following limits: 10 documents per site No workspaces 100,000 rows To determine if you\u2019re on a legacy personal site, click on your site name (@your-name) in the top left. Personal sites on the legacy plan will say \u201cPersonal Site (Legacy)\u201d in the dropdown menu. On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page .","title":"Limits"},{"location":"limits/#limits","text":"To help you assess whether Grist will work for a use-case you have in mind, here is a list of limitations that apply to its operation.","title":"Limits"},{"location":"limits/#number-of-documents","text":"On all plans, the number of documents is not limited. To prevent accidental abuse of the system by automation tools, team sites may be limited to 1,000 documents. If you encounter such a limit for legitimate use, please contact support to increase it. Older free plans had a limit of ten documents. Learn more about legacy limits .","title":"Number of documents"},{"location":"limits/#number-of-collaborators","text":"For team sites on all plans, there is no limit on the number of team members that may be added to the site. For paid plans, the number of team members determines the price. See our pricing page for details. Team members added to your team site may inherit access to workspaces or documents within that organization. Learn more about team sharing . On both personal and team sites, each document may be shared with up to 2 free guests who do not affect the plan price even on paid plans.","title":"Number of collaborators"},{"location":"limits/#number-of-tables-per-document","text":"There is a limit of 500 tables allowed per document. This is a soft limit. If you find yourself with a large number of tables, consider merging ones that have the same structure. For example, if you have a table for each product type, consider using a single table with the product type as an extra column.","title":"Number of tables per document"},{"location":"limits/#rows-per-document","text":"On the Free plan, documents have a limit of 5,000 rows. The limit for Pro and Business plans is 100,000 and 150,000 rows, respectively. Documents are also subject to data size limits, as described below.","title":"Rows per document"},{"location":"limits/#data-size","text":"There is a hard limit to a document\u2019s total data size, determined as the row limit multiplied by 2KB. This means that documents on the Free plan have a data size limit of 10MB, with Pro and Business plan documents having limits of 200MB and 300MB, respectively. This value corresponds approximately to the size of the data in CSV format. You can see a document\u2019s current data size on the \u2018Raw Data\u2019 page . For memory and performance reasons, there\u2019s a recommended data size limit of 20MB. Documents beyond 20MB may slow down or run into memory limits depending on their complexity. As an example, a document with 100,000 rows and 24 numeric columns would reach this recommended limit. To help optimize formulas on large documents, you can use the built-in formula timer . Attachments are counted separately. Attachments plus data in a single document are limited to 1GB on all plans.","title":"Data size"},{"location":"limits/#uploads","text":"Uploads are limited to 50MB, both for attachments and to import data. Note that an import within this limit may result in a document that exceeds the document size limit, in which case the upload is still likely to fail.","title":"Uploads"},{"location":"limits/#api-limits","text":"Free plans are limited to 5,000 API calls per document per day. Pro and Business plans raise the limit to 40,000 and 60,000 calls per document per day, respectively. Free plans may be rate limited to 5 API requests per second per document. The team plan does not impose such a rate limit. Separately, there is a concurrency limit of 10 for all plans: if 10 authorized API requests are currently being processed for a particular document, any other API requests will be rejected (with HTTP status code 429) until at least one of the original requests completes. A client that waits for one request to complete before sending the next would not hit this limit (assuming it is the sole client accessing the document). The size of the body of any individual API request is limited to 1MB. In particular, this means that requests adding or updating multiple records may need to be split into batches that fit within this limit.","title":"API limits"},{"location":"limits/#document-availability","text":"From time to time, during upgrades and operational transitions, individual Grist documents may become inaccessible for a period of some seconds. Please bear this in mind when using Grist\u2019s API.","title":"Document availability"},{"location":"limits/#legacy-limits","text":"Older free personal plans have the following limits: 10 documents per site No workspaces 100,000 rows To determine if you\u2019re on a legacy personal site, click on your site name (@your-name) in the top left. Personal sites on the legacy plan will say \u201cPersonal Site (Legacy)\u201d in the dropdown menu. On the current Free plan, all personal and team sites share the same limits as described above and on our pricing page .","title":"Legacy limits"},{"location":"linking-widgets/","text":"Linking Page Widgets # One big reason for placing more than one widget on a page is that widgets can be linked. When linked, selecting a record in one widget will cause another widget to update and show only the data related to the selected record. For example, let\u2019s say you have a table of Departments in a company, and a table of Employees , with each employee tied to some department. You can have one Table widget listing departments and serving as a selector for a second Table widget listing employees: Previous Next To create this, first create a page with a Table widget for Departments data, as described in Page widgets . Then in the \u201cAdd New\u201d menu, select the \u201cAdd Widget to Page\u201d option to add another Table widget for Employees data. In the widget picker, use the \u201cSelect By\u201d dropdown and choose the \u201cDEPARTMENTS\u201d widget added in the first step. That\u2019s all it takes: now selecting a department in the first table will cause the second table to show only the employees in that department. Note that this relies on the Employees table having a column of type Reference with the target table of Departments . See Reference columns . Types of linking # Linking widgets only works when there is a relationship between records in the underlying data. There are several kinds of relationships supported. Same-record linking # Most directly you can link two widgets which show data from the same underlying table, usually linking a Table to a Card. This allows you to select a record in the Table widget and see more details of that record in the linked Card widget. For example, you can add a Card of Employees data and link it to an existing Table widget \u201cEMPLOYEES\u201d: When you select a record in the table, the new \u201cEMPLOYEES Card\u201d widget shows a card for the selected record. For another example of such linking, see the \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d and the Customizing Layout section in the associated tutorial. Filter linking # As in the Employee-Department example, when one table has a reference to another (i.e. a column of type \u201cReference\u201d), the second table can serve as a selector for the first. Essentially, selecting a record can automatically filter another widget to show only those records that refer to the selected record. In the example shown earlier, the Employees table has a \u201cReference\u201d column pointing to the Departments table, so a list of departments can serve as a selector for employees. When a department is selected, only the employees in that department will be shown. The \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d provides another example, where selecting a contact shows only the conversations with that contact. It\u2019s also described in the tutorial . When the target of linking is a Chart widget, you get dynamic charts that reflect data associated with the selected record. For example, you could link a pie chart to a department to show the sum of salaries for each job position in the selected department. Previous Next Indirect linking # Whenever a table A has a reference to B , A can be used in place of B as the source of linking. Instead of considering the currently selected record in A , linking will consider the referenced record in B as the selection. For instance, a Table widget showing Employees can serve as a selector for a Card widget showing data from Departments , using the fact that an employee record contains a \u201cDepartment\u201d reference. In the widget picker, if you select data from Departments , you\u2019ll see a \u201cSelect By\u201d option \u201cEMPLOYEES \u2022 Department\u201d: When you select an employee, you\u2019ll see the details of that employee\u2019s department. Multiple reference columns # When a table that\u2019s the target of linking has multiple reference columns, you may need to choose which one to use for linking. For instance, a Flight record might have fields \u2018DepartureAirport\u2019 and \u2018ArrivalAirport\u2019, each of which is a Reference to the table Airports . When you select an airport in a table, you can have a choice whether to show all flights departing from this airport or all flights arriving to it. The \u201cSelect By\u201d widget will show both options to choose from: Linking summary tables # When widgets display summarized data, as described in Summary tables , they can also be linked to the underlying data, data that references the underlying data, and to other summary tables. For example, you can summarize the table Employees by job position, and include the count of employees for each position, the average salary, or other summary data. You can also link the Employees table to it, so that selecting a position shows all employees in that position. Furthermore, you can link another summary table. For instance, you can summarize employees by position and gender, and link that to the summary by position. When selecting a job position, you can then see a summary by gender for that position. This is also convenient with charts. In this example, you see a pie chart with the average salary for men vs women for the selected job position. As you click different positions, the pie chart updates to reflect the selected one. More examples of such linking can be found in the Analyze and visualize tutorial. Lastly, tables that reference a summary table\u2019s underlying data may now be linked to the summary table itself. In the image below, the Champion Dog\u2019s table has a reference column to the Breeder table. The Breeder table is being summarized in the top right widget by the \u201cCountry\u201d column. Because Champion Dog references Breeder, you may add a widget of Champion Dogs that selects by a summary table of Breeder data. Changing link settings # After a widget is added, you can view and change its link settings from the right panel. One way to get to it is to click on the three-dots icon on the top right of the widget, and click on the \u201cData selection\u201d menu option: This opens the side panel, which shows what data is shown, and which widget, if any, serves as its selector. You can change the \u201cSelect By\u201d setting here, or click the green \u201cEdit Data Selection\u201d button, and change it in the widget picker dialog.","title":"Linking widgets"},{"location":"linking-widgets/#linking-page-widgets","text":"One big reason for placing more than one widget on a page is that widgets can be linked. When linked, selecting a record in one widget will cause another widget to update and show only the data related to the selected record. For example, let\u2019s say you have a table of Departments in a company, and a table of Employees , with each employee tied to some department. You can have one Table widget listing departments and serving as a selector for a second Table widget listing employees: Previous Next To create this, first create a page with a Table widget for Departments data, as described in Page widgets . Then in the \u201cAdd New\u201d menu, select the \u201cAdd Widget to Page\u201d option to add another Table widget for Employees data. In the widget picker, use the \u201cSelect By\u201d dropdown and choose the \u201cDEPARTMENTS\u201d widget added in the first step. That\u2019s all it takes: now selecting a department in the first table will cause the second table to show only the employees in that department. Note that this relies on the Employees table having a column of type Reference with the target table of Departments . See Reference columns .","title":"Linking Page Widgets"},{"location":"linking-widgets/#types-of-linking","text":"Linking widgets only works when there is a relationship between records in the underlying data. There are several kinds of relationships supported.","title":""},{"location":"linking-widgets/#same-record-linking","text":"Most directly you can link two widgets which show data from the same underlying table, usually linking a Table to a Card. This allows you to select a record in the Table widget and see more details of that record in the linked Card widget. For example, you can add a Card of Employees data and link it to an existing Table widget \u201cEMPLOYEES\u201d: When you select a record in the table, the new \u201cEMPLOYEES Card\u201d widget shows a card for the selected record. For another example of such linking, see the \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d and the Customizing Layout section in the associated tutorial.","title":"Same-record linking"},{"location":"linking-widgets/#filter-linking","text":"As in the Employee-Department example, when one table has a reference to another (i.e. a column of type \u201cReference\u201d), the second table can serve as a selector for the first. Essentially, selecting a record can automatically filter another widget to show only those records that refer to the selected record. In the example shown earlier, the Employees table has a \u201cReference\u201d column pointing to the Departments table, so a list of departments can serve as a selector for employees. When a department is selected, only the employees in that department will be shown. The \u201cLightweight CRM\u201d document in \u201cExamples & Templates\u201d provides another example, where selecting a contact shows only the conversations with that contact. It\u2019s also described in the tutorial . When the target of linking is a Chart widget, you get dynamic charts that reflect data associated with the selected record. For example, you could link a pie chart to a department to show the sum of salaries for each job position in the selected department. Previous Next","title":"Filter linking"},{"location":"linking-widgets/#indirect-linking","text":"Whenever a table A has a reference to B , A can be used in place of B as the source of linking. Instead of considering the currently selected record in A , linking will consider the referenced record in B as the selection. For instance, a Table widget showing Employees can serve as a selector for a Card widget showing data from Departments , using the fact that an employee record contains a \u201cDepartment\u201d reference. In the widget picker, if you select data from Departments , you\u2019ll see a \u201cSelect By\u201d option \u201cEMPLOYEES \u2022 Department\u201d: When you select an employee, you\u2019ll see the details of that employee\u2019s department.","title":"Indirect linking"},{"location":"linking-widgets/#multiple-reference-columns","text":"When a table that\u2019s the target of linking has multiple reference columns, you may need to choose which one to use for linking. For instance, a Flight record might have fields \u2018DepartureAirport\u2019 and \u2018ArrivalAirport\u2019, each of which is a Reference to the table Airports . When you select an airport in a table, you can have a choice whether to show all flights departing from this airport or all flights arriving to it. The \u201cSelect By\u201d widget will show both options to choose from:","title":"Multiple reference columns"},{"location":"linking-widgets/#linking-summary-tables","text":"When widgets display summarized data, as described in Summary tables , they can also be linked to the underlying data, data that references the underlying data, and to other summary tables. For example, you can summarize the table Employees by job position, and include the count of employees for each position, the average salary, or other summary data. You can also link the Employees table to it, so that selecting a position shows all employees in that position. Furthermore, you can link another summary table. For instance, you can summarize employees by position and gender, and link that to the summary by position. When selecting a job position, you can then see a summary by gender for that position. This is also convenient with charts. In this example, you see a pie chart with the average salary for men vs women for the selected job position. As you click different positions, the pie chart updates to reflect the selected one. More examples of such linking can be found in the Analyze and visualize tutorial. Lastly, tables that reference a summary table\u2019s underlying data may now be linked to the summary table itself. In the image below, the Champion Dog\u2019s table has a reference column to the Breeder table. The Breeder table is being summarized in the top right widget by the \u201cCountry\u201d column. Because Champion Dog references Breeder, you may add a widget of Champion Dogs that selects by a summary table of Breeder data.","title":"Linking summary tables"},{"location":"linking-widgets/#changing-link-settings","text":"After a widget is added, you can view and change its link settings from the right panel. One way to get to it is to click on the three-dots icon on the top right of the widget, and click on the \u201cData selection\u201d menu option: This opens the side panel, which shows what data is shown, and which widget, if any, serves as its selector. You can change the \u201cSelect By\u201d setting here, or click the green \u201cEdit Data Selection\u201d button, and change it in the widget picker dialog.","title":"Changing link settings"},{"location":"newsletters/","text":"Grist for the Mill # Welcome to our monthly newsletter of updates and tips for Grist users. The phrase \u201cGrist for the mill\u201d means \u201cuseful experience, material, or knowledge\u201d, perfectly reflecting its purpose. To receive the newsletter, sign up for Grist . May 2024 Newsletter : New Grist Business plan, a formula timer utility, draggable conditionals and admin console improvements! April 2024 Newsletter : Filtering reference and choice dropdown lists, an admin console for self-hosters. March 2024 Newsletter : Improvements to forms, new file formats, and a boot page for self-hosters. February 2024 Newsletter : Community spotlight: new widgets, fun experiments, and in-depth explorations. January 2024 Newsletter : Grist Forms are here! Getting data into Grist just got easier. December 2023 Newsletter : Grist 2023 year in review, forms user testing and community showcase. November 2023 Newsletter : Open referenced records with a click, hang out with us on Discord, and add column types more easily. October 2023 Newsletter : New formula shortcuts, two experimental widgets, colorful calendar events and much more! September 2023 Newsletter : Calendar widget, two new templates, and API endpoint for making SQL queries. August 2023 Newsletter : Grist CSV Viewer! Llama AI support! July 2023 Newsletter : AI Formula Assistant launched! June 2023 Newsletter : Selector row highlighting, new templates, and community contributions. May 2023 Newsletter : Column and widget description, webhooks, and a vote for the best flashcards. April 2023 Newsletter : Flashcards contest, a way to prank friends with Grist, and a new experiment in publishing spreadsheet data. March 2023 Newsletter : Minimizing widgets, in-product tutorials, and a flashcards custom widget! February 2023 Newsletter : Grist in more languages, and a sneak peek into dev passion projects. January 2023 Newsletter : Grist en Fran\u00e7ais, Espa\u00f1ol, Portugu\u00eas, and Deutsch! Plus expanding widgets and access rules improvements. December 2022 Newsletter : New date filter with calendar, and snapshots in grist-core. November 2022 Newsletter : Grist experiment with AI formula writing, and improved sort and filter. October 2022 Newsletter : Quick sums, duplicate tables, and new API methods. September 2022 Newsletter : Dark Mode \ud83d\udd76 + improved user management. August 2022 Newsletter : Free team sites, conditional row styles, and better formula help. July 2022 Newsletter : Formula cheat sheet and summary tables in raw data. June 2022 Newsletter : Filtering by range, and new PEEK() function. May 2022 Newsletter : Raw data tables, and new summary table linking. April 2022 Newsletter : Rich text editor, and more cell styles. March 2022 Newsletter : Conditional formatting, new API method, and the new Sprouts Program. February 2022 Newsletter : Custom widgets menu, 2FA, access rules improvements, and cell context menu. January 2022 Newsletter : Managing template document tours, and 4 new templates, including an inventory manager. December 2021 Newsletter : Zapier instant triggers, and four new templates. November 2021 Newsletter : Import column mapping, filter on hidden columns, and more sorting options. October 2021 Newsletter : Editing choices, inline links, and enhanced import previews. September 2021 Newsletter : Improved imports, global currencies, and more ways to integrate. August 2021 Newsletter : Reference Lists, Embedding, Templates, Pabbly, and more. July 2021 Newsletter : Colors, Time and Authorship Stamps, and Google Sheets Integration. June 2021 Newsletter : Introducing Freeze Columns and a timesheet-tracking template. May 2021 Newsletter : Choice Lists, 3-step Tutorial on Reference Columns. April 2021 Newsletter : A 4-Step Guide to Link Keys. March 2021 Newsletter : Access Rules. February 2021 Newsletter : Improved Mobile Support, Totals. January 2021 Newsletter : Task Management, Find a Consultant, Be a Consultant. December 2020 Newsletter : Maps, Access Control, Compare Previous. November 2020 Newsletter : Open Source, Improved Attachments, Compare Versions. October 2020 Newsletter : Printing Support, Mailing Labels, Open Source Beta. September 2020 Newsletter : Public Access, Payroll Tracking. August 2020 Newsletter : Invoices, Custom Widgets, Document Lists. July 2020 Newsletter : Number Format Options, Prepare Emails with Formulas. June 2020 Newsletter : Work on Complex Changes, Automatic Backups. May 2020 Newsletter : Copy as Template, Better URLs, NY Tech Meetup.","title":"Newsletters"},{"location":"newsletters/#grist-for-the-mill","text":"Welcome to our monthly newsletter of updates and tips for Grist users. The phrase \u201cGrist for the mill\u201d means \u201cuseful experience, material, or knowledge\u201d, perfectly reflecting its purpose. To receive the newsletter, sign up for Grist . May 2024 Newsletter : New Grist Business plan, a formula timer utility, draggable conditionals and admin console improvements! April 2024 Newsletter : Filtering reference and choice dropdown lists, an admin console for self-hosters. March 2024 Newsletter : Improvements to forms, new file formats, and a boot page for self-hosters. February 2024 Newsletter : Community spotlight: new widgets, fun experiments, and in-depth explorations. January 2024 Newsletter : Grist Forms are here! Getting data into Grist just got easier. December 2023 Newsletter : Grist 2023 year in review, forms user testing and community showcase. November 2023 Newsletter : Open referenced records with a click, hang out with us on Discord, and add column types more easily. October 2023 Newsletter : New formula shortcuts, two experimental widgets, colorful calendar events and much more! September 2023 Newsletter : Calendar widget, two new templates, and API endpoint for making SQL queries. August 2023 Newsletter : Grist CSV Viewer! Llama AI support! July 2023 Newsletter : AI Formula Assistant launched! June 2023 Newsletter : Selector row highlighting, new templates, and community contributions. May 2023 Newsletter : Column and widget description, webhooks, and a vote for the best flashcards. April 2023 Newsletter : Flashcards contest, a way to prank friends with Grist, and a new experiment in publishing spreadsheet data. March 2023 Newsletter : Minimizing widgets, in-product tutorials, and a flashcards custom widget! February 2023 Newsletter : Grist in more languages, and a sneak peek into dev passion projects. January 2023 Newsletter : Grist en Fran\u00e7ais, Espa\u00f1ol, Portugu\u00eas, and Deutsch! Plus expanding widgets and access rules improvements. December 2022 Newsletter : New date filter with calendar, and snapshots in grist-core. November 2022 Newsletter : Grist experiment with AI formula writing, and improved sort and filter. October 2022 Newsletter : Quick sums, duplicate tables, and new API methods. September 2022 Newsletter : Dark Mode \ud83d\udd76 + improved user management. August 2022 Newsletter : Free team sites, conditional row styles, and better formula help. July 2022 Newsletter : Formula cheat sheet and summary tables in raw data. June 2022 Newsletter : Filtering by range, and new PEEK() function. May 2022 Newsletter : Raw data tables, and new summary table linking. April 2022 Newsletter : Rich text editor, and more cell styles. March 2022 Newsletter : Conditional formatting, new API method, and the new Sprouts Program. February 2022 Newsletter : Custom widgets menu, 2FA, access rules improvements, and cell context menu. January 2022 Newsletter : Managing template document tours, and 4 new templates, including an inventory manager. December 2021 Newsletter : Zapier instant triggers, and four new templates. November 2021 Newsletter : Import column mapping, filter on hidden columns, and more sorting options. October 2021 Newsletter : Editing choices, inline links, and enhanced import previews. September 2021 Newsletter : Improved imports, global currencies, and more ways to integrate. August 2021 Newsletter : Reference Lists, Embedding, Templates, Pabbly, and more. July 2021 Newsletter : Colors, Time and Authorship Stamps, and Google Sheets Integration. June 2021 Newsletter : Introducing Freeze Columns and a timesheet-tracking template. May 2021 Newsletter : Choice Lists, 3-step Tutorial on Reference Columns. April 2021 Newsletter : A 4-Step Guide to Link Keys. March 2021 Newsletter : Access Rules. February 2021 Newsletter : Improved Mobile Support, Totals. January 2021 Newsletter : Task Management, Find a Consultant, Be a Consultant. December 2020 Newsletter : Maps, Access Control, Compare Previous. November 2020 Newsletter : Open Source, Improved Attachments, Compare Versions. October 2020 Newsletter : Printing Support, Mailing Labels, Open Source Beta. September 2020 Newsletter : Public Access, Payroll Tracking. August 2020 Newsletter : Invoices, Custom Widgets, Document Lists. July 2020 Newsletter : Number Format Options, Prepare Emails with Formulas. June 2020 Newsletter : Work on Complex Changes, Automatic Backups. May 2020 Newsletter : Copy as Template, Better URLs, NY Tech Meetup.","title":"Grist for the Mill"},{"location":"on-demand-tables/","text":"On-Demand Tables # On-demand tables are an experimental feature The design of on-demand tables may change. For example, configuration options may be added, or aspects of the behavior of on-demand tables may be changed entirely. A defining feature of spreadsheets is the ability to update cells instantly as data they depend on changes. But sometimes a table is just a store of data, without any fancy calculations needed. In that case, you can choose to mark the table as \u201cOn-Demand\u201d in Grist. Grist can then serve data from that table faster, and make certain optimizations that may be helpful as the table gets large and regular tables become slow. When a table is marked \u201cOn-Demand\u201d: Data in the table will not generally be available for use in formulas. The table remains available for viewing and editing, but with caveats. Here\u2019s what you need to know about viewing data: Viewing is limited to 10000 rows at a time. Subsets of the table\u2019s rows can be selected by linking widgets , as for regular tables. You can expect good performance of linked widgets when the subset of the table\u2019s rows is less than 10000 rows, even if the full table is much larger. Here\u2019s what you need to know about editing data: You can edit data as normal in an On-Demand table. Automatic updates of anything that depends on that data simply won\u2019t happen. After edits, you need to reload the webpage to see everything updated. Here are some reasons you might make a table On-Demand: You want to make summaries and charts of slices of a large dataset using linked widgets . You are storing a lot of data in the table, and all you need to do with it is read parts of it back out via the API. Make an On-Demand Table # To convert a table to be an \u201cOn Demand\u201d table, open the right panel, pick the \u201cTable\u201d panel, and the \u201cData\u201d section. Click on \u201cAdvanced Settings\u201d and you should see a \u201cMake On-Demand\u201d button. If you change your mind, and don\u2019t want the table to be \u201cOn-Demand\u201d anymore, you can find a button to undo this setting in the same place: Changing a table to become \u201cOn-Demand\u201d or to stop being \u201cOn-Demand\u201d will force the document to reload for all users viewing it at that moment. Formulas, References and On-Demand Tables # In general, formulas and on-demand tables don\u2019t go together. That said, if you\u2019re careful you can use the following very simple formulas: $column - where the column mentioned is not itself a formula. This copies data from another column verbatim. $reference.column - where reference is a reference column , and column is not itself a formula. This formula support is enough to unlock Grist\u2019s linking widgets feature, which is why it is present. In general, if you try using formulas and On-Demand tables, you are setting yourself up for sadness. Remember, like any edit of an On-Demand table, when you add or change a formula column you\u2019ll generally need to reload to see cell values updated. Some type conversions, such as converting a column to be a reference, are not effective for On-Demand tables. So it is important to perform such conversions before making a table On-Demand. From formulas in regular tables, you cannot access the content of on-demand tables.","title":"On-Demand tables"},{"location":"on-demand-tables/#on-demand-tables","text":"On-demand tables are an experimental feature The design of on-demand tables may change. For example, configuration options may be added, or aspects of the behavior of on-demand tables may be changed entirely. A defining feature of spreadsheets is the ability to update cells instantly as data they depend on changes. But sometimes a table is just a store of data, without any fancy calculations needed. In that case, you can choose to mark the table as \u201cOn-Demand\u201d in Grist. Grist can then serve data from that table faster, and make certain optimizations that may be helpful as the table gets large and regular tables become slow. When a table is marked \u201cOn-Demand\u201d: Data in the table will not generally be available for use in formulas. The table remains available for viewing and editing, but with caveats. Here\u2019s what you need to know about viewing data: Viewing is limited to 10000 rows at a time. Subsets of the table\u2019s rows can be selected by linking widgets , as for regular tables. You can expect good performance of linked widgets when the subset of the table\u2019s rows is less than 10000 rows, even if the full table is much larger. Here\u2019s what you need to know about editing data: You can edit data as normal in an On-Demand table. Automatic updates of anything that depends on that data simply won\u2019t happen. After edits, you need to reload the webpage to see everything updated. Here are some reasons you might make a table On-Demand: You want to make summaries and charts of slices of a large dataset using linked widgets . You are storing a lot of data in the table, and all you need to do with it is read parts of it back out via the API.","title":"On-Demand Tables"},{"location":"on-demand-tables/#make-an-on-demand-table","text":"To convert a table to be an \u201cOn Demand\u201d table, open the right panel, pick the \u201cTable\u201d panel, and the \u201cData\u201d section. Click on \u201cAdvanced Settings\u201d and you should see a \u201cMake On-Demand\u201d button. If you change your mind, and don\u2019t want the table to be \u201cOn-Demand\u201d anymore, you can find a button to undo this setting in the same place: Changing a table to become \u201cOn-Demand\u201d or to stop being \u201cOn-Demand\u201d will force the document to reload for all users viewing it at that moment.","title":"Make an On-Demand Table"},{"location":"on-demand-tables/#formulas-references-and-on-demand-tables","text":"In general, formulas and on-demand tables don\u2019t go together. That said, if you\u2019re careful you can use the following very simple formulas: $column - where the column mentioned is not itself a formula. This copies data from another column verbatim. $reference.column - where reference is a reference column , and column is not itself a formula. This formula support is enough to unlock Grist\u2019s linking widgets feature, which is why it is present. In general, if you try using formulas and On-Demand tables, you are setting yourself up for sadness. Remember, like any edit of an On-Demand table, when you add or change a formula column you\u2019ll generally need to reload to see cell values updated. Some type conversions, such as converting a column to be a reference, are not effective for On-Demand tables. So it is important to perform such conversions before making a table On-Demand. From formulas in regular tables, you cannot access the content of on-demand tables.","title":"Formulas, References and On-Demand Tables"},{"location":"page-widgets/","text":"Pages & widgets # Unlike traditional spreadsheets, in Grist you can create multiple views of the same data, and display multiple data sets on one page. This allows you to create useful dashboards and custom applications tailored to your needs. Pages # In Grist, you organize your document into \u201cpages\u201d. These are listed in the left panel, with collapsible groups. You may rearrange and group pages in the left panel by dragging them to suit your needs. You can rename, remove, or duplicate pages using the three-dots menu next to the page name in the list. Renaming the page does not edit data tables\u2019 names or widget titles. See changing widget below to learn how to edit table and widget names. Duplicating a page duplicates views of data and does not duplicate the data itself. Removing a page does not delete data. When removing the last view of data, you will be asked if you want to delete only the view, but not the data itself; or if you want to delete both the page and the underlying data table(s). Learn more about your document\u2019s data in the raw data page . Note that pages can also be renamed by clicking the page name on top of the screen. Using the opener icon ( ) near the top of the left panel, you can collapse the panel to show only the initials of each page, leaving more screen space to view your data. To add a new page, use the \u201cAdd New\u201d button, and click \u201cAdd Page\u201d. At that point, you\u2019ll get to choose the page widget to include in the new page. Using Emojis in Page and Widget Names You can add any emoji to a Page or Widget name. The keyboard shortcut to open the emoji keyboard is Windows Logo + . (period) on PC or Command + Control + Space on Mac. You can also copy/paste an online source like Emojipedia . When a Page name starts with an emoji, it will replace the page icon. Page widgets # A page contains sections, such as tables or charts, which we call \u201cpage widgets\u201d. Each page widget shows data from one table. A page may contain more than one page widget, and you can arrange and link them to create useful layouts. Here are the supported kinds of page widgets. The salient features of each one are described on separate pages. Table : similar to the spreadsheet grid and a good way to see many records at once. Card : shows a single record in a form-like layout which you can customize. Card List : uses the same layout options as a card, displays a scrollable list of records. Chart : plots data on a chart with support for several different chart types. Calendar : displays event data in a calendar view. Custom : inserts a custom webpage, optionally granting it access to the document\u2019s data. There is a special page called raw data that lists all data tables in your document and summarizes your document\u2019s usage statistics. Navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu. Widget picker # The menu opened by the \u201cAdd New\u201d button has options \u201cAdd Page\u201d and \u201cAdd Widget to Page\u201d. In either case, you\u2019ll see the \u201cpage widget picker\u201d where you can choose your desired widget: You can select the type of widget and the table of data to show (or \u201cNew Table\u201d to create a new table). The \u201csummary\u201d icon ( ) allows you to summarize data . When adding a widget to an existing page, you\u2019ll also see a \u201cSelect By\u201d option, which allows linking this widget to another one already on the page. This process is described in greater detail in Linking widgets . Once you\u2019ve added widgets, they can be moved around and resized, as described in Custom Layouts . Changing widget or its data # If you\u2019d like to change a widget or the data it displays after it\u2019s added, you may do so. Click the three-dots button on the top right of your widget, and select \u201cWidget options\u201d. This opens the right-side panel. Click on \u201cChange Widget\u201d. You can then use the widget picker to change the widget or the data it displays. You may also edit the widget\u2019s title or add a description. Renaming widgets # You can rename widgets in several ways. We saw in the section above that you can edit a widget\u2019s title or add a description from the Widget tab of the Creator Panel. Another way is to click on the widget title above a widget. From here, you can edit the widget\u2019s title, the underlying data table\u2019s name or add a description. By default, the widget title is the data table\u2019s name. To override this, enter a new title under \u2018Widget Title\u2019. Learn more about data tables in the raw data page . Configuring field lists # Although different kinds of page widgets look very different, they all represent a list of records. Any of the widget types can be used to show the same underlying data. In a Table , each record is represented by a row, and columns represent the same kind of value for each record. Note that the raw data page lists all data tables. In a Card List , each row of the underlying data is shown as a card. Each column in the data corresponds to a field in this card. When talking about a Card widget, we\u2019ll use the term \u201cfield\u201d , which conceptually is the same as a \u201ccolumn\u201d in a Table widget. A Card is just like a Card List, but shows only one row of data at a time. In a Chart , each row of the underlying data table becomes a graphical element, such as a point on a line chart, a bar in a bar chart, or a slice of a pie chart. In this context, the columns of our data table are better known as data \u201cseries\u201d . Click on the opener icon ( ) to open the right panel. Depending on the currently-selected widget, you might see a tab for configuring a Column, Field, or Series. These are not different in substance, but different terms make more sense for different widgets. Clicking on the widget tab (highlighted in green in the images above), you\u2019ll see subtabs for \u201cWidget\u201d, \u201cSort & Filter\u201d, and \u201cData\u201d. We\u2019ll focus on the first one: \u201cWidget\u201d. You\u2019ll see options specific to the type of the selected widget, and below that two lists: \u201cVisible Columns\u201d and \u201cHidden Columns\u201d. The \u201cHidden Columns\u201d are the columns available in the data, but not shown in this widget. In a Card, these lists would show up as \u201cVisible Fields\u201d / \u201cHidden Fields\u201d. In a chart, they show up as \u201cVisible Series\u201d / \u201cHidden Series\u201d. These lists allow you to include, exclude, or rearrange fields in a widget. As you move your mouse over the items in the list, use the \u201ceye\u201d icons that pop up to show or hide them. Alternatively, you can select several items using the checkboxes, and hide or show them together. This ordered list of fields can be used to customize any of the page widget types. It has a particular importance in the Chart widget , where different chart types and options require you to place series in a certain order in the \u201cVisible Series\u201d list to ensure your data is plotted correctly.","title":"Pages & widgets"},{"location":"page-widgets/#pages-widgets","text":"Unlike traditional spreadsheets, in Grist you can create multiple views of the same data, and display multiple data sets on one page. This allows you to create useful dashboards and custom applications tailored to your needs.","title":""},{"location":"page-widgets/#pages","text":"In Grist, you organize your document into \u201cpages\u201d. These are listed in the left panel, with collapsible groups. You may rearrange and group pages in the left panel by dragging them to suit your needs. You can rename, remove, or duplicate pages using the three-dots menu next to the page name in the list. Renaming the page does not edit data tables\u2019 names or widget titles. See changing widget below to learn how to edit table and widget names. Duplicating a page duplicates views of data and does not duplicate the data itself. Removing a page does not delete data. When removing the last view of data, you will be asked if you want to delete only the view, but not the data itself; or if you want to delete both the page and the underlying data table(s). Learn more about your document\u2019s data in the raw data page . Note that pages can also be renamed by clicking the page name on top of the screen. Using the opener icon ( ) near the top of the left panel, you can collapse the panel to show only the initials of each page, leaving more screen space to view your data. To add a new page, use the \u201cAdd New\u201d button, and click \u201cAdd Page\u201d. At that point, you\u2019ll get to choose the page widget to include in the new page. Using Emojis in Page and Widget Names You can add any emoji to a Page or Widget name. The keyboard shortcut to open the emoji keyboard is Windows Logo + . (period) on PC or Command + Control + Space on Mac. You can also copy/paste an online source like Emojipedia . When a Page name starts with an emoji, it will replace the page icon.","title":"Pages"},{"location":"page-widgets/#page-widgets","text":"A page contains sections, such as tables or charts, which we call \u201cpage widgets\u201d. Each page widget shows data from one table. A page may contain more than one page widget, and you can arrange and link them to create useful layouts. Here are the supported kinds of page widgets. The salient features of each one are described on separate pages. Table : similar to the spreadsheet grid and a good way to see many records at once. Card : shows a single record in a form-like layout which you can customize. Card List : uses the same layout options as a card, displays a scrollable list of records. Chart : plots data on a chart with support for several different chart types. Calendar : displays event data in a calendar view. Custom : inserts a custom webpage, optionally granting it access to the document\u2019s data. There is a special page called raw data that lists all data tables in your document and summarizes your document\u2019s usage statistics. Navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu.","title":"Page widgets"},{"location":"page-widgets/#widget-picker","text":"The menu opened by the \u201cAdd New\u201d button has options \u201cAdd Page\u201d and \u201cAdd Widget to Page\u201d. In either case, you\u2019ll see the \u201cpage widget picker\u201d where you can choose your desired widget: You can select the type of widget and the table of data to show (or \u201cNew Table\u201d to create a new table). The \u201csummary\u201d icon ( ) allows you to summarize data . When adding a widget to an existing page, you\u2019ll also see a \u201cSelect By\u201d option, which allows linking this widget to another one already on the page. This process is described in greater detail in Linking widgets . Once you\u2019ve added widgets, they can be moved around and resized, as described in Custom Layouts .","title":"Widget picker"},{"location":"page-widgets/#changing-widget-or-its-data","text":"If you\u2019d like to change a widget or the data it displays after it\u2019s added, you may do so. Click the three-dots button on the top right of your widget, and select \u201cWidget options\u201d. This opens the right-side panel. Click on \u201cChange Widget\u201d. You can then use the widget picker to change the widget or the data it displays. You may also edit the widget\u2019s title or add a description.","title":"Changing widget or its data"},{"location":"page-widgets/#renaming-widgets","text":"You can rename widgets in several ways. We saw in the section above that you can edit a widget\u2019s title or add a description from the Widget tab of the Creator Panel. Another way is to click on the widget title above a widget. From here, you can edit the widget\u2019s title, the underlying data table\u2019s name or add a description. By default, the widget title is the data table\u2019s name. To override this, enter a new title under \u2018Widget Title\u2019. Learn more about data tables in the raw data page .","title":"Renaming widgets"},{"location":"page-widgets/#configuring-field-lists","text":"Although different kinds of page widgets look very different, they all represent a list of records. Any of the widget types can be used to show the same underlying data. In a Table , each record is represented by a row, and columns represent the same kind of value for each record. Note that the raw data page lists all data tables. In a Card List , each row of the underlying data is shown as a card. Each column in the data corresponds to a field in this card. When talking about a Card widget, we\u2019ll use the term \u201cfield\u201d , which conceptually is the same as a \u201ccolumn\u201d in a Table widget. A Card is just like a Card List, but shows only one row of data at a time. In a Chart , each row of the underlying data table becomes a graphical element, such as a point on a line chart, a bar in a bar chart, or a slice of a pie chart. In this context, the columns of our data table are better known as data \u201cseries\u201d . Click on the opener icon ( ) to open the right panel. Depending on the currently-selected widget, you might see a tab for configuring a Column, Field, or Series. These are not different in substance, but different terms make more sense for different widgets. Clicking on the widget tab (highlighted in green in the images above), you\u2019ll see subtabs for \u201cWidget\u201d, \u201cSort & Filter\u201d, and \u201cData\u201d. We\u2019ll focus on the first one: \u201cWidget\u201d. You\u2019ll see options specific to the type of the selected widget, and below that two lists: \u201cVisible Columns\u201d and \u201cHidden Columns\u201d. The \u201cHidden Columns\u201d are the columns available in the data, but not shown in this widget. In a Card, these lists would show up as \u201cVisible Fields\u201d / \u201cHidden Fields\u201d. In a chart, they show up as \u201cVisible Series\u201d / \u201cHidden Series\u201d. These lists allow you to include, exclude, or rearrange fields in a widget. As you move your mouse over the items in the list, use the \u201ceye\u201d icons that pop up to show or hide them. Alternatively, you can select several items using the checkboxes, and hide or show them together. This ordered list of fields can be used to customize any of the page widget types. It has a particular importance in the Chart widget , where different chart types and options require you to place series in a certain order in the \u201cVisible Series\u201d list to ensure your data is plotted correctly.","title":"Configuring field lists"},{"location":"python/","text":"Python # Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Python formulas are evaluated in a sandbox, without internet access, and without a persistent filesystem. Supported Python versions # We currently support two versions of Python: Python 3 (specifically 3.11 at the time of writing) Python 2 (specifically 2.7) Newly created documents on our hosted service use Python 3 by default, while older documents (created before November 2021 approximately) use Python 2 by default. To tell which version of Python a specific document uses, look at its Document Settings . The Engine setting may be python2 , python3 , or blank. A blank setting implies python2 . If you have editing rights on a document, you can change the Engine setting, and the document will then reload with all formulas now interpreted using the version of Python you have specified. We recommend caution in doing so. A formula that works as intended in one version of Python may give errors in another, or (worse) give the wrong results. Warning Some formulas may fail or give wrong results if used with a version of Python that is different from the one for which they were written. Python 2 reached its end of life in January 2020, so if you look online for python help, the answers you find are more and more likely to be for Python 3. If you have a document that uses Python 2, and you\u2019d like to switch it to use Python 3, we recommend reading Testing the effect of changing Python versions and Differences between Python versions . Be sure to check all tables and columns, and both regular formalas and trigger formulas. We\u2019d be interested to hear your experience, and to help with any problems, on the community forum . Self-hosted Grist may use any version of Python you configure it with, but bear in mind we actively test only the supported versions. Testing the effect of changing Python versions # Grist has some features that can help you evaluate the consequences of changing the Python version a document uses. The Work on a Copy feature is useful to experiment with changing the Python version without affecting your document until you are ready. There is a \u201cCompare with original\u201d option that will let you visualize which cells changed, if any. Be sure to look at all tables and columns. The Activity tab of Document History (with \u201cAll Tables\u201d selected) lets you review in more detail what has changed. Be careful to test any trigger formulas you may have, since the Python code in them won\u2019t be exercised until you specifically trigger these formulas. You can use the code viewer to quickly remind yourself of all formulas in a document, so you can systematically check them all. Differences between Python versions # There are important differences between Python 2 and 3. Formulas may need to be changed in order to give the same results when switching between Python versions. There are many online resources such as this compatibility cheatsheet which can help figure out what the issue is when you hit a difference, and get ideas on how to resolve it. Here, we list common cases we\u2019ve seen in Grist formulas. Division of whole numbers # In Python 2, dividing whole numbers gives a whole number, so 9 / 2 is 4 . In Python 3, it is 4.5 . For a spreadsheet, this is a much more sensible answer, but if you rely on the Python 2 behavior, we suggest you switch to the // operator which is consistent between versions ( 9 // 2 is 4 for both). For example the General Ledger template had a Python 2 formula for computing the quarter from a date (so a Date of 2021-08-15 gave a Quarter of 2021 Q3 ) as follows: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) / 3) when switching to Python 3, this needed correcting to: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) // 3) Otherwise Quarters became fractional! Some imports are reorganized # Python has a useful standard library, but some parts of it were moved around between Python 2 and 3. For example, several of our templates have formulas to construct URLs, to open custom searches for example, or to open a pre-populated email with calculated To , CC , and Subject values. Python has handy helpers for constructing URLs, but they moved around a bit between Python versions. Our Lightweight CRM example had a Python 2 formula like this to kick off a search for emails in Gmail: from urllib import quote_plus \"Gmail search https://mail.google.com/mail/u/0/#search/%s\" % quote_plus($Email) In Python 3, the import line needed changing to: from urllib.parse import quote_plus Subtle change in rounding # Python 3 switches the built-in function round() from rounding the way many people learned in school (where when rounding .5 , you always round up) to what is called \u201cbanker\u2019s rounding\u201d (where you round from .5 to the nearest even number). This is generally accepted as an improvement, mitigating a bias to larger numbers that can become significant at scale. But it could be a surprise to see numbers change like this in an established document. If you really need Python 2\u2019s rounding, replace any calls to Python\u2019s round function with the Excel-compatible ROUND function. For example: round($val, 2) would be replaced with: ROUND($val, 2) Unicode text handling # Python 2 does not shine at handling international text and emojis. We have mitigated many problems by setting the default encoding to utf8 for all documents. Nevertheless, when switching from Python 2 to Python 3, you may see type changes or errors. Consider this Python 2 formula to generate a one-way hash of an email address: import hashlib hashlib.sha256($Email).hexdigest() In Python 3 this fails with TypeError: Unicode-objects must be encoded before hashing , which can be resolved by replacing $Email with $Email.encode() : import hashlib hashlib.sha256($Email.encode()).hexdigest()","title":"Python versions"},{"location":"python/#python","text":"Grist formulas are written in Python, the most popular language for data science. The entirety of Python\u2019s standard library is available to you. For those with a spreadsheet background, we\u2019ve also added a suite of Excel-like functions, with all-uppercase names. Here\u2019s the full list of functions . Python formulas are evaluated in a sandbox, without internet access, and without a persistent filesystem.","title":"Python"},{"location":"python/#supported-python-versions","text":"We currently support two versions of Python: Python 3 (specifically 3.11 at the time of writing) Python 2 (specifically 2.7) Newly created documents on our hosted service use Python 3 by default, while older documents (created before November 2021 approximately) use Python 2 by default. To tell which version of Python a specific document uses, look at its Document Settings . The Engine setting may be python2 , python3 , or blank. A blank setting implies python2 . If you have editing rights on a document, you can change the Engine setting, and the document will then reload with all formulas now interpreted using the version of Python you have specified. We recommend caution in doing so. A formula that works as intended in one version of Python may give errors in another, or (worse) give the wrong results. Warning Some formulas may fail or give wrong results if used with a version of Python that is different from the one for which they were written. Python 2 reached its end of life in January 2020, so if you look online for python help, the answers you find are more and more likely to be for Python 3. If you have a document that uses Python 2, and you\u2019d like to switch it to use Python 3, we recommend reading Testing the effect of changing Python versions and Differences between Python versions . Be sure to check all tables and columns, and both regular formalas and trigger formulas. We\u2019d be interested to hear your experience, and to help with any problems, on the community forum . Self-hosted Grist may use any version of Python you configure it with, but bear in mind we actively test only the supported versions.","title":"Supported Python versions"},{"location":"python/#testing-the-effect-of-changing-python-versions","text":"Grist has some features that can help you evaluate the consequences of changing the Python version a document uses. The Work on a Copy feature is useful to experiment with changing the Python version without affecting your document until you are ready. There is a \u201cCompare with original\u201d option that will let you visualize which cells changed, if any. Be sure to look at all tables and columns. The Activity tab of Document History (with \u201cAll Tables\u201d selected) lets you review in more detail what has changed. Be careful to test any trigger formulas you may have, since the Python code in them won\u2019t be exercised until you specifically trigger these formulas. You can use the code viewer to quickly remind yourself of all formulas in a document, so you can systematically check them all.","title":"Testing the effect of changing Python versions"},{"location":"python/#differences-between-python-versions","text":"There are important differences between Python 2 and 3. Formulas may need to be changed in order to give the same results when switching between Python versions. There are many online resources such as this compatibility cheatsheet which can help figure out what the issue is when you hit a difference, and get ideas on how to resolve it. Here, we list common cases we\u2019ve seen in Grist formulas.","title":"Differences between Python versions"},{"location":"python/#division-of-whole-numbers","text":"In Python 2, dividing whole numbers gives a whole number, so 9 / 2 is 4 . In Python 3, it is 4.5 . For a spreadsheet, this is a much more sensible answer, but if you rely on the Python 2 behavior, we suggest you switch to the // operator which is consistent between versions ( 9 // 2 is 4 for both). For example the General Ledger template had a Python 2 formula for computing the quarter from a date (so a Date of 2021-08-15 gave a Quarter of 2021 Q3 ) as follows: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) / 3) when switching to Python 3, this needed correcting to: \"%s Q%s\" % ($Date.year, CEILING($Date.month, 3) // 3) Otherwise Quarters became fractional!","title":"Division of whole numbers"},{"location":"python/#some-imports-are-reorganized","text":"Python has a useful standard library, but some parts of it were moved around between Python 2 and 3. For example, several of our templates have formulas to construct URLs, to open custom searches for example, or to open a pre-populated email with calculated To , CC , and Subject values. Python has handy helpers for constructing URLs, but they moved around a bit between Python versions. Our Lightweight CRM example had a Python 2 formula like this to kick off a search for emails in Gmail: from urllib import quote_plus \"Gmail search https://mail.google.com/mail/u/0/#search/%s\" % quote_plus($Email) In Python 3, the import line needed changing to: from urllib.parse import quote_plus","title":"Some imports are reorganized"},{"location":"python/#subtle-change-in-rounding","text":"Python 3 switches the built-in function round() from rounding the way many people learned in school (where when rounding .5 , you always round up) to what is called \u201cbanker\u2019s rounding\u201d (where you round from .5 to the nearest even number). This is generally accepted as an improvement, mitigating a bias to larger numbers that can become significant at scale. But it could be a surprise to see numbers change like this in an established document. If you really need Python 2\u2019s rounding, replace any calls to Python\u2019s round function with the Excel-compatible ROUND function. For example: round($val, 2) would be replaced with: ROUND($val, 2)","title":"Subtle change in rounding"},{"location":"python/#unicode-text-handling","text":"Python 2 does not shine at handling international text and emojis. We have mitigated many problems by setting the default encoding to utf8 for all documents. Nevertheless, when switching from Python 2 to Python 3, you may see type changes or errors. Consider this Python 2 formula to generate a one-way hash of an email address: import hashlib hashlib.sha256($Email).hexdigest() In Python 3 this fails with TypeError: Unicode-objects must be encoded before hashing , which can be resolved by replacing $Email with $Email.encode() : import hashlib hashlib.sha256($Email.encode()).hexdigest()","title":"Unicode text handling"},{"location":"raw-data/","text":"Raw data page # The raw data page is a special page that lists all data tables in your document and summarizes your document\u2019s usage statistics. From your document, navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu. Unlike other pages , the layout in the raw data page cannot be customized. From the list of data tables, you can find the data table\u2019s name and id, and remove data. Note that removing a data table from this page will delete data and remove it from all pages. This is different from other pages where it is possible to remove a view of data and not delete the data itself. Click on a data table to open it. Note that in the creator panel the widget type cannot be changed. Renaming the widget also renames the data table. Because raw data is intended to show all data, columns cannot be hidden, either. However, columns can be rearranged, deleted, created, and modified. For creators, this view may make it easier to edit data structure, add formulas , conditional formatting , and so on. Duplicating Data # Tables can be duplicated from the Raw Data page. Click the three-dot icon to the right of the table you wish to duplicate then select \u2018Duplicate Table\u2019 from the menu. By default, a duplicated table will only contain the table structure, not the data. If you wish to copy all data in addition to the table structure, be sure to check the box prior to clicking \u2018Save\u2019. If the original table has any access rules, those rules will not be duplicated. Only the document\u2019s default rules will apply to the copied table. The duplicate table is a new table that is not linked to the original. Meaning, if you update data in the copy, the original table will not be updated, and vice versa. Note that instead of duplicating tables, it\u2019s usually better to segment data by adding a new column. Let\u2019s use expenses as an example. Rather than having separate tables for each month\u2019s expenses, it is better to include all data in a single table and create a new column called \u201cMonth\u201d to segment rows into months. In general, if you have multiple tables with near identical columns, this is an indicator that the data could all be in the same table. Doing so may make data analysis easier. Usage # Usage statistics are summarized beneath the list of data tables. Note that usage applies to the entire document, not individual tables. Learn more about document limits .","title":"Raw data page"},{"location":"raw-data/#raw-data-page","text":"The raw data page is a special page that lists all data tables in your document and summarizes your document\u2019s usage statistics. From your document, navigate to the raw data page by clicking on the Raw Data link in the bottom left of the pages menu. Unlike other pages , the layout in the raw data page cannot be customized. From the list of data tables, you can find the data table\u2019s name and id, and remove data. Note that removing a data table from this page will delete data and remove it from all pages. This is different from other pages where it is possible to remove a view of data and not delete the data itself. Click on a data table to open it. Note that in the creator panel the widget type cannot be changed. Renaming the widget also renames the data table. Because raw data is intended to show all data, columns cannot be hidden, either. However, columns can be rearranged, deleted, created, and modified. For creators, this view may make it easier to edit data structure, add formulas , conditional formatting , and so on.","title":"Raw data page"},{"location":"raw-data/#duplicating-data","text":"Tables can be duplicated from the Raw Data page. Click the three-dot icon to the right of the table you wish to duplicate then select \u2018Duplicate Table\u2019 from the menu. By default, a duplicated table will only contain the table structure, not the data. If you wish to copy all data in addition to the table structure, be sure to check the box prior to clicking \u2018Save\u2019. If the original table has any access rules, those rules will not be duplicated. Only the document\u2019s default rules will apply to the copied table. The duplicate table is a new table that is not linked to the original. Meaning, if you update data in the copy, the original table will not be updated, and vice versa. Note that instead of duplicating tables, it\u2019s usually better to segment data by adding a new column. Let\u2019s use expenses as an example. Rather than having separate tables for each month\u2019s expenses, it is better to include all data in a single table and create a new column called \u201cMonth\u201d to segment rows into months. In general, if you have multiple tables with near identical columns, this is an indicator that the data could all be in the same table. Doing so may make data analysis easier.","title":"Duplicating Data"},{"location":"raw-data/#usage","text":"Usage statistics are summarized beneath the list of data tables. Note that usage applies to the entire document, not individual tables. Learn more about document limits .","title":"Usage"},{"location":"record-cards/","text":"Record Cards # Record Cards are a quick, easy way to view a record\u2019s details. To view a record\u2019s details as a card, hover over the row number and click the arrow icon that appears. Select \u2018View as card\u2019 from the dropdown menu. This opens an editable card view of the record\u2019s details. If your table contains a reference or reference list column , you can click the link icon to open the linked record\u2019s card. A record card will open to display record data for the referenced record. Editing a Record Card\u2019s Layout # You can edit a record card\u2019s layout from the Raw Data page. Click the card icon to open. You can drag and drop fields to rearrange, resize and add/delete fields from the view. Learn more about editing card layouts . Disabling a Record Card # You can also disable a record card from the Raw Data page. To disable a record card for a particular table, click the three-dot icon to the right of the table name then select \u2018Disable Record Card\u2019 from the dropdown.","title":"Record Cards"},{"location":"record-cards/#record-cards","text":"Record Cards are a quick, easy way to view a record\u2019s details. To view a record\u2019s details as a card, hover over the row number and click the arrow icon that appears. Select \u2018View as card\u2019 from the dropdown menu. This opens an editable card view of the record\u2019s details. If your table contains a reference or reference list column , you can click the link icon to open the linked record\u2019s card. A record card will open to display record data for the referenced record.","title":"Record Cards"},{"location":"record-cards/#editing-a-record-cards-layout","text":"You can edit a record card\u2019s layout from the Raw Data page. Click the card icon to open. You can drag and drop fields to rearrange, resize and add/delete fields from the view. Learn more about editing card layouts .","title":"Editing a Record Card's Layout"},{"location":"record-cards/#disabling-a-record-card","text":"You can also disable a record card from the Raw Data page. To disable a record card for a particular table, click the three-dot icon to the right of the table name then select \u2018Disable Record Card\u2019 from the dropdown.","title":"Disabling a Record Card"},{"location":"references-lookups/","text":"Using References and Lookups in Formulas # Reference and Reference List columns in Grist allow one table to create an explicit reference to another. A common example of this is seen in the Class Enrollment template. On the Staff page, we have a list of staff members. On the classes page, we have a reference column labeled Instructor that references the records on our Staff page. Keep in mind, it\u2019s not just referencing the Full Name column but the entire record associated with the selected instructor. Reference columns and dot notation # Using a Reference column within a formula can make it easy to get any data from the referenced record. To do this, we use dot notation. It uses the format $A.B where A is the name of the reference column and B is the name of the column in the referenced table that we want to pull data from. Let\u2019s see this in action on the Enrollment View page of the Class Enrollment template. Dot notation is used in the Class_Times column of the ENROLLMENTS table, found at the bottom right of the Enrollment View page. We can see that the Class_Times column is using a formula with dot notation. Using the format $A.B described above, we can figure out that Class is the name of the reference column and Times is the name of the column in the referenced table. Let\u2019s track this back to where it\u2019s pulling from - since the reference column is Class, we can look at that column\u2019s information to find out what table it is pulling from. The Class column references data from the Classes table. Therefore, the Class_Times column is pulling from the Times column of the Classes table. Chaining # If the reference lookup returns a reference, this can be chained. Perhaps we want to add the Instructor\u2019s phone number to the Enrollments table. We can use the Class reference column to pull the instructor\u2019s information from the Classes table. As you can see in the screenshot above, the instructor column is a reference column itself. If we follow the format from before, our dot notation would be $Class.Instructor but the Instructor column points to the entire record of the instructor so we need to tell it what information we want from this record, creating a chain. The instructor column references the Staff table so we navigate there to find out what column we need to pull information from in order to get the phone number. The column that contains the instructor\u2019s phone number is Phone. Putting this all together, our dot notation for the instructor\u2019s phone number would be $Class.Instructor.Phone What happens if we leave our formulas as $Class.Instructor ? You will see a numeric record ID of the record in the Staff table that the Instructor column points to. That\u2019s what a reference column really stores. If you change the type of this formula column to Reference, you will be able to select a column to show, such as the Full Name. Another way to see the name is to chain the dot-notation, as we did for phone: $Class.Instructor.Full_Name . lookupOne # Another way to point to a record is using Table.lookupOne(...) function. lookupOne allows you to look up a record by some fields, similar to Excel\u2019s VLOOKUP. In fact, Grist\u2019s version of VLOOKUP is merely an alias for lookupOne. lookupOne is rarely useful in Grist, because using a Reference type column is usually the preferred solution to connect records. However, on some occasions, lookupOne can be useful. One situation is when you have two sets of data which overlap even though they represent something different and perhaps come from different sources. An example of this can be found in our Event Sponsors + Attendees (References and Lookups) document which is a modified version of the Event Sponsors + Attendees template, available in our template gallery . Let\u2019s say that you run an event and have a list of registered attendees, as well as Sponsors. Registered attendees are stored in the All Registrations table, perhaps populated via a form integration. Sponsors are listed in a separate table, with fields related to their sponsorship, and perhaps maintained by another team. Both tables contain email addresses which identify attendees and sponsors. Sometimes a sponsor may register to attend the event. In that case, you\u2019ll have an Attendee record with an email address that also appears in the Sponsors table. That\u2019s useful to know for someone looking at the attendee list. You can lookup a record in the sponsors table by email address by using a lookupOne formula. The Sponsor column in the All Registrations table does just that using this formula: Sponsors.lookupOne(Contact_Email=$Registration_Email) This formula is looking to see if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. The general format for a lookupOne formula is: [Table_Name].lookupOne([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Such a formula returns a reference. In the screenshot above, you can see the lookup result returns Sponsors[#] . The number it returns between square brackets is the record ID of the lookup result. Where it returns Sponsors[0] , no match was found. It\u2019s often a good idea to create a column for the lookup result and change its type to Reference, as you see in the screenshot below. Then, if there is a match, the reference column will point to the entire matched record. Like any reference column, you can select which field from that record to show. In this example, it shows the Company field of the matched record in the Sponsors table. lookupOne and dot notation # Because lookupOne is creating a reference to a record, we can use dot notation to look up additional fields in that record. In the example above, Sponsors.lookupOne(Contact_Email=$Registration_Email) is checking if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. What if we also wanted to look up the sponsor level? We can add .Sponsor_Level to the lookupOne formula, and, if a match is found, look up the value in the sponsor level column for the matched record. The entire formula would be Sponsors.lookupOne(Contact_Email=$Registration_Email).Sponsor_Level . Now, we have the Sponsor Level listed in the All Registrations table for those attendees whose emails also appear on the sponsor list. lookupOne and sort_by # When the lookupOne function encounters multiple matching results, it returns the first one by row ID. The optional sort_by parameter can be used to sort these results by another field, to determine which one would be returned as the first match. You can also prefix the column ID with \u201c-\u201d to reverse the order. For instance, consider this example from the Class Enrollment template. This template tracks enrollment for extracurricular and other classes - logging information for students, families, and staff. On this page, we have a list of students and their respective information. Additionally, we have a Families page that outlines the parent of each student and we\u2019d like to find which student in each family is the oldest. So, we would create an oldest student column. Then, the following formula would look at the Students table, find the specific students associated with each family, sort them by their birthday, and return the one student with the earliest birthday: Students.lookupOne(Family=$id, sort_by=\"Birthday\") In this case, this would return: Raddon, Brockie. Alternatively, if we want to find the youngest student, the formula would include \u201c-\u201c: Students.lookupOne(Family=$id, sort_by=\"-Birthday\u201d) In this case, this would return: Raddon, Care. Understanding record sets # Sometimes a record may reference multiple records in another table. Multiple references can be made with a Reference List Column. A great example of this is seen on the Habit Tracker template. On the Habits + Goals page, we have a list of habits and a goal for how often we wish to complete that habit. On the Habit Tracker page, we have a Reference List column labeled Habits Completed that references the records on our Habits + Goals page. The only difference between a Reference column and a Reference List column is the ability to select multiple references. This creates a set of records which can be used in formulas. Reference lists and dot notation # Similar to references, you can use Dot Notation with reference lists. Building on our prior example of attendees at a conference, suppose we have a list of registrants for an event and want to find the balance for each registrant. To do this, we can use dot notation. Here, $Registrants is a reference list. Our Great Outdoors Expo has 4 registrants. We can see the list of registrants in the Registrants column. This list is a reference to the Name column of the All Registrants table. With a reference list, dot-notation returns a list of all the selected field; $Registrants.Balance is a list of the Balances for each attendee in the list of $Registrants . This follows the format $[A].[B] where [A] is the name of the Reference List column and [B] is the name of the column in the referenced table you wish to pull data from. We\u2019ll learn how to find the sum of these balances in Working with Record Sets . lookupRecords # You can also get a list of references using lookupRecords . The formula for lookupRecords follows this format: [Table_Name].lookupRecords([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Suppose we want a list of the events attended by each person in our Email List table. We can use lookupRecords to do this. First, we need to lookup records where the email listed in the All Registrations table matches an email in this list. Then, find the event associated with each of those records. Following the format above, our initial formula is: All_Registrations.lookupRecords(Registration_Email=$Email) All_Registrations.lookupRecords(Registration_Email=$Email) returns a list of record IDs for each record in the All Registrations table where the Registration Email matches the Email in this row of the Email List table. Next, we need to find the Event associated with each of these records. To do this, we can use dot notation. All_Registrations.lookupRecords(Registration_Email=$Email).Event will return the value from the Event column for each record found. We saw similar results using the lookupOne function. It\u2019s helpful to change the column type to Reference List, as you see in the screenshot below. Then, if there is a match, the reference list column will point to the entire record for each match. Like any reference list column, you can select which field you want to show for the matched records. In this example, it shows the Event field of the Events table for each matched record in the Attendees table. Reverse lookups # LookupRecords works a bit differently if a reference exists between two tables. With a reverse lookup, we can use the record ID to find a record. Every row has a numeric id (available as $id in formulas) that is unique within that table. You can reveal the ID by adding a formula column where formula is $id Let\u2019s take a look at the Registrants column of the Events table. The formula used here is All_Registrations.lookupRecords(Event=$id) . We use the id to find a match because in the All Registrations table, the Event column is a reference column which means its value is a record\u2019s id. Because All_Registrations.Event is a reference column pointing to an Event record in the Events table, we can match the id stored in the reference column to the ids of records in the Events table. That\u2019s why the argument in the formula is Event=$id . We use the existing reference, just in reverse - hence the name, Reverse Lookup. If you\u2019d like a video walkthrough of a reverse lookup, we have an example in our Build with Grist Webinar - Trigger Formulas v. Formulas . Working with record sets # lookupRecords can also be used within other formulas. SUM() can be useful to find a sum of all numbers in a list of records. Once you find your list of records using the lookupRecords function and dot notation, you can use SUM() to sum all values returned, like you see in this formula: SUM(Table.lookupRecords(Column_A=$Column_B).Column_C) You can also do this on a reference list because a reference list is the same thing, a list of records. SUM($RefList.Column) In the Reference lists and dot notation section, we used the Registrants column and dot notation to find the balance for each person in our list of Registrants. We can use SUM() with our prior formula to find the total balance. SUM($Registrants.Balance) We can also use lookupRecords to get the list of references, rather than using a reference list column, then find the sum of the balance for all registrants. This method is used in the Ticket Revenue column of the Events table using the following formula: SUM(All_Registrations.lookupRecords(Event=$id).Balance) All_Registrations.lookupRecords(Event=$id).Balance finds all records in the All Registrations table where the Event column matches the ID of the row in this table, Events. Using dot notation, we find the Balance for each of the records found. Then SUM() sums the balances of all records found. You can also iterate through a Reference List using a Python for loop. An example of this can be seen in the Balance (\u2018for\u2019 loop) column in the Events table. When iterating, each element is a Reference so dot-notation can be used here as well. To find the sum of the balance for all registrants, we use the following formula: SUM(person.Balance for person in $Registrants) This does the same thing as our lookupRecords formula we saw above. $Registrants is our reference list. For each record ( person ) in our list of Registrants, we find the Balance. Then, sum all balances together. In this formula, person is a variable that represents each element in our list and could be replaced with any other variable. If you\u2019d like to learn more about Data Structures and List Comprehension in Python 3, Python.org is a great resource. len() can be useful to get the number of items within a list. Once you find your list of records using the lookupRecords function, you can use len() to count the number of records returned, like you see in this formula: len(Table.lookupRecords(Column_A=$Column_B)) You can also do this on a reference list. len($RefList) We want to see how many events our Sponsors have attended. We can use lookupRecords to do this. The following formula is used in the Events Attended column of the Sponsors table. len(All_Registrations.lookupRecords(Sponsor=$id)) Let\u2019s break down the two parts of this formula, working from the inside out. All_Registrations.lookupRecords(Sponsor=$id) is looking for matches where the record in the Sponsor column of the All Registrations table has the same ID as the record in this row of the Sponsors table. All records in the All Registrations table that match are added to a list of records. Try writing the formula without len() to see what Grist returns. It should look something like this. That\u2019s a list of records. len() counts how many records are in that list. We can also include multiple arguments in a lookupRecords formula. An example of this can be found in the Count column of the Classes table of the Class Enrollment template. This column shows us how many students are enrolled in each class. The formula used here is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This lookup uses two fields. It will look for records in the Enrollment table where Status is \u201cConfirmed\u201d and the Class column matches the ID of the row in this table. Because the Class column is referencing the Classes table, we use the record ID $id in the lookup. Finally, len() counts the items in the list returned by Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\") . Average, min and max are a few of the other functions that can be used with lookupRecords formulas. See all available functions on our Function reference page.","title":"References and Lookups"},{"location":"references-lookups/#using-references-and-lookups-in-formulas","text":"Reference and Reference List columns in Grist allow one table to create an explicit reference to another. A common example of this is seen in the Class Enrollment template. On the Staff page, we have a list of staff members. On the classes page, we have a reference column labeled Instructor that references the records on our Staff page. Keep in mind, it\u2019s not just referencing the Full Name column but the entire record associated with the selected instructor.","title":"Using References and Lookups in Formulas"},{"location":"references-lookups/#reference-columns-and-dot-notation","text":"Using a Reference column within a formula can make it easy to get any data from the referenced record. To do this, we use dot notation. It uses the format $A.B where A is the name of the reference column and B is the name of the column in the referenced table that we want to pull data from. Let\u2019s see this in action on the Enrollment View page of the Class Enrollment template. Dot notation is used in the Class_Times column of the ENROLLMENTS table, found at the bottom right of the Enrollment View page. We can see that the Class_Times column is using a formula with dot notation. Using the format $A.B described above, we can figure out that Class is the name of the reference column and Times is the name of the column in the referenced table. Let\u2019s track this back to where it\u2019s pulling from - since the reference column is Class, we can look at that column\u2019s information to find out what table it is pulling from. The Class column references data from the Classes table. Therefore, the Class_Times column is pulling from the Times column of the Classes table.","title":"Reference columns and dot notation"},{"location":"references-lookups/#chaining","text":"If the reference lookup returns a reference, this can be chained. Perhaps we want to add the Instructor\u2019s phone number to the Enrollments table. We can use the Class reference column to pull the instructor\u2019s information from the Classes table. As you can see in the screenshot above, the instructor column is a reference column itself. If we follow the format from before, our dot notation would be $Class.Instructor but the Instructor column points to the entire record of the instructor so we need to tell it what information we want from this record, creating a chain. The instructor column references the Staff table so we navigate there to find out what column we need to pull information from in order to get the phone number. The column that contains the instructor\u2019s phone number is Phone. Putting this all together, our dot notation for the instructor\u2019s phone number would be $Class.Instructor.Phone What happens if we leave our formulas as $Class.Instructor ? You will see a numeric record ID of the record in the Staff table that the Instructor column points to. That\u2019s what a reference column really stores. If you change the type of this formula column to Reference, you will be able to select a column to show, such as the Full Name. Another way to see the name is to chain the dot-notation, as we did for phone: $Class.Instructor.Full_Name .","title":"Chaining"},{"location":"references-lookups/#lookupone","text":"Another way to point to a record is using Table.lookupOne(...) function. lookupOne allows you to look up a record by some fields, similar to Excel\u2019s VLOOKUP. In fact, Grist\u2019s version of VLOOKUP is merely an alias for lookupOne. lookupOne is rarely useful in Grist, because using a Reference type column is usually the preferred solution to connect records. However, on some occasions, lookupOne can be useful. One situation is when you have two sets of data which overlap even though they represent something different and perhaps come from different sources. An example of this can be found in our Event Sponsors + Attendees (References and Lookups) document which is a modified version of the Event Sponsors + Attendees template, available in our template gallery . Let\u2019s say that you run an event and have a list of registered attendees, as well as Sponsors. Registered attendees are stored in the All Registrations table, perhaps populated via a form integration. Sponsors are listed in a separate table, with fields related to their sponsorship, and perhaps maintained by another team. Both tables contain email addresses which identify attendees and sponsors. Sometimes a sponsor may register to attend the event. In that case, you\u2019ll have an Attendee record with an email address that also appears in the Sponsors table. That\u2019s useful to know for someone looking at the attendee list. You can lookup a record in the sponsors table by email address by using a lookupOne formula. The Sponsor column in the All Registrations table does just that using this formula: Sponsors.lookupOne(Contact_Email=$Registration_Email) This formula is looking to see if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. The general format for a lookupOne formula is: [Table_Name].lookupOne([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Such a formula returns a reference. In the screenshot above, you can see the lookup result returns Sponsors[#] . The number it returns between square brackets is the record ID of the lookup result. Where it returns Sponsors[0] , no match was found. It\u2019s often a good idea to create a column for the lookup result and change its type to Reference, as you see in the screenshot below. Then, if there is a match, the reference column will point to the entire matched record. Like any reference column, you can select which field from that record to show. In this example, it shows the Company field of the matched record in the Sponsors table.","title":"lookupOne"},{"location":"references-lookups/#lookupone-and-dot-notation","text":"Because lookupOne is creating a reference to a record, we can use dot notation to look up additional fields in that record. In the example above, Sponsors.lookupOne(Contact_Email=$Registration_Email) is checking if a Contact Email from the Sponsors table matches a Registration Email from the All Registrations table. What if we also wanted to look up the sponsor level? We can add .Sponsor_Level to the lookupOne formula, and, if a match is found, look up the value in the sponsor level column for the matched record. The entire formula would be Sponsors.lookupOne(Contact_Email=$Registration_Email).Sponsor_Level . Now, we have the Sponsor Level listed in the All Registrations table for those attendees whose emails also appear on the sponsor list.","title":"lookupOne and dot notation"},{"location":"references-lookups/#lookupone-and-sort_by","text":"When the lookupOne function encounters multiple matching results, it returns the first one by row ID. The optional sort_by parameter can be used to sort these results by another field, to determine which one would be returned as the first match. You can also prefix the column ID with \u201c-\u201d to reverse the order. For instance, consider this example from the Class Enrollment template. This template tracks enrollment for extracurricular and other classes - logging information for students, families, and staff. On this page, we have a list of students and their respective information. Additionally, we have a Families page that outlines the parent of each student and we\u2019d like to find which student in each family is the oldest. So, we would create an oldest student column. Then, the following formula would look at the Students table, find the specific students associated with each family, sort them by their birthday, and return the one student with the earliest birthday: Students.lookupOne(Family=$id, sort_by=\"Birthday\") In this case, this would return: Raddon, Brockie. Alternatively, if we want to find the youngest student, the formula would include \u201c-\u201c: Students.lookupOne(Family=$id, sort_by=\"-Birthday\u201d) In this case, this would return: Raddon, Care.","title":"lookupOne and sort_by"},{"location":"references-lookups/#understanding-record-sets","text":"Sometimes a record may reference multiple records in another table. Multiple references can be made with a Reference List Column. A great example of this is seen on the Habit Tracker template. On the Habits + Goals page, we have a list of habits and a goal for how often we wish to complete that habit. On the Habit Tracker page, we have a Reference List column labeled Habits Completed that references the records on our Habits + Goals page. The only difference between a Reference column and a Reference List column is the ability to select multiple references. This creates a set of records which can be used in formulas.","title":"Understanding record sets"},{"location":"references-lookups/#reference-lists-and-dot-notation","text":"Similar to references, you can use Dot Notation with reference lists. Building on our prior example of attendees at a conference, suppose we have a list of registrants for an event and want to find the balance for each registrant. To do this, we can use dot notation. Here, $Registrants is a reference list. Our Great Outdoors Expo has 4 registrants. We can see the list of registrants in the Registrants column. This list is a reference to the Name column of the All Registrants table. With a reference list, dot-notation returns a list of all the selected field; $Registrants.Balance is a list of the Balances for each attendee in the list of $Registrants . This follows the format $[A].[B] where [A] is the name of the Reference List column and [B] is the name of the column in the referenced table you wish to pull data from. We\u2019ll learn how to find the sum of these balances in Working with Record Sets .","title":"Reference lists and dot notation"},{"location":"references-lookups/#lookuprecords","text":"You can also get a list of references using lookupRecords . The formula for lookupRecords follows this format: [Table_Name].lookupRecords([A]=$[B]) [Table_Name] is the name of the table you want to lookup data in. [A] is the column in the table being looked up (named at the beginning of the formula) and [B] is the column in the current table / the table you are entering the formula in. Suppose we want a list of the events attended by each person in our Email List table. We can use lookupRecords to do this. First, we need to lookup records where the email listed in the All Registrations table matches an email in this list. Then, find the event associated with each of those records. Following the format above, our initial formula is: All_Registrations.lookupRecords(Registration_Email=$Email) All_Registrations.lookupRecords(Registration_Email=$Email) returns a list of record IDs for each record in the All Registrations table where the Registration Email matches the Email in this row of the Email List table. Next, we need to find the Event associated with each of these records. To do this, we can use dot notation. All_Registrations.lookupRecords(Registration_Email=$Email).Event will return the value from the Event column for each record found. We saw similar results using the lookupOne function. It\u2019s helpful to change the column type to Reference List, as you see in the screenshot below. Then, if there is a match, the reference list column will point to the entire record for each match. Like any reference list column, you can select which field you want to show for the matched records. In this example, it shows the Event field of the Events table for each matched record in the Attendees table.","title":"lookupRecords"},{"location":"references-lookups/#reverse-lookups","text":"LookupRecords works a bit differently if a reference exists between two tables. With a reverse lookup, we can use the record ID to find a record. Every row has a numeric id (available as $id in formulas) that is unique within that table. You can reveal the ID by adding a formula column where formula is $id Let\u2019s take a look at the Registrants column of the Events table. The formula used here is All_Registrations.lookupRecords(Event=$id) . We use the id to find a match because in the All Registrations table, the Event column is a reference column which means its value is a record\u2019s id. Because All_Registrations.Event is a reference column pointing to an Event record in the Events table, we can match the id stored in the reference column to the ids of records in the Events table. That\u2019s why the argument in the formula is Event=$id . We use the existing reference, just in reverse - hence the name, Reverse Lookup. If you\u2019d like a video walkthrough of a reverse lookup, we have an example in our Build with Grist Webinar - Trigger Formulas v. Formulas .","title":"Reverse lookups"},{"location":"references-lookups/#working-with-record-sets","text":"lookupRecords can also be used within other formulas. SUM() can be useful to find a sum of all numbers in a list of records. Once you find your list of records using the lookupRecords function and dot notation, you can use SUM() to sum all values returned, like you see in this formula: SUM(Table.lookupRecords(Column_A=$Column_B).Column_C) You can also do this on a reference list because a reference list is the same thing, a list of records. SUM($RefList.Column) In the Reference lists and dot notation section, we used the Registrants column and dot notation to find the balance for each person in our list of Registrants. We can use SUM() with our prior formula to find the total balance. SUM($Registrants.Balance) We can also use lookupRecords to get the list of references, rather than using a reference list column, then find the sum of the balance for all registrants. This method is used in the Ticket Revenue column of the Events table using the following formula: SUM(All_Registrations.lookupRecords(Event=$id).Balance) All_Registrations.lookupRecords(Event=$id).Balance finds all records in the All Registrations table where the Event column matches the ID of the row in this table, Events. Using dot notation, we find the Balance for each of the records found. Then SUM() sums the balances of all records found. You can also iterate through a Reference List using a Python for loop. An example of this can be seen in the Balance (\u2018for\u2019 loop) column in the Events table. When iterating, each element is a Reference so dot-notation can be used here as well. To find the sum of the balance for all registrants, we use the following formula: SUM(person.Balance for person in $Registrants) This does the same thing as our lookupRecords formula we saw above. $Registrants is our reference list. For each record ( person ) in our list of Registrants, we find the Balance. Then, sum all balances together. In this formula, person is a variable that represents each element in our list and could be replaced with any other variable. If you\u2019d like to learn more about Data Structures and List Comprehension in Python 3, Python.org is a great resource. len() can be useful to get the number of items within a list. Once you find your list of records using the lookupRecords function, you can use len() to count the number of records returned, like you see in this formula: len(Table.lookupRecords(Column_A=$Column_B)) You can also do this on a reference list. len($RefList) We want to see how many events our Sponsors have attended. We can use lookupRecords to do this. The following formula is used in the Events Attended column of the Sponsors table. len(All_Registrations.lookupRecords(Sponsor=$id)) Let\u2019s break down the two parts of this formula, working from the inside out. All_Registrations.lookupRecords(Sponsor=$id) is looking for matches where the record in the Sponsor column of the All Registrations table has the same ID as the record in this row of the Sponsors table. All records in the All Registrations table that match are added to a list of records. Try writing the formula without len() to see what Grist returns. It should look something like this. That\u2019s a list of records. len() counts how many records are in that list. We can also include multiple arguments in a lookupRecords formula. An example of this can be found in the Count column of the Classes table of the Class Enrollment template. This column shows us how many students are enrolled in each class. The formula used here is: len(Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\")) This lookup uses two fields. It will look for records in the Enrollment table where Status is \u201cConfirmed\u201d and the Class column matches the ID of the row in this table. Because the Class column is referencing the Classes table, we use the record ID $id in the lookup. Finally, len() counts the items in the list returned by Enrollments.lookupRecords(Class=$id, Status=\"Confirmed\") . Average, min and max are a few of the other functions that can be used with lookupRecords formulas. See all available functions on our Function reference page.","title":"Working with record sets"},{"location":"register-as-consultant/","text":"Register to be a Grist consultant # .creg-form { line-height: initial; } .form-group .control-label { font-weight: normal; } .creg-button { margin: 24px 0; background-color: #11b683; border: none; color: white; } .creg-button:hover { background-color: #009058; border: none; color: white; } #creg-submitted, #creg-error { display: none; margin-top: 40px; } Are you a freelancer? Do you enjoy building in Grist and organizing data? We\u2019d be glad to support you, and to add you to our list of independent Grist consultants. Please fill out the information below, and we\u2019ll get in touch with you. Your Name: Email: Phone: Company: Website: Your technical background: Excel: None Beginner Intermediate Advanced SQL: None Beginner Intermediate Advanced Python: None Beginner Intermediate Advanced Javascript: None Beginner Intermediate Advanced Grist: None Beginner Intermediate Advanced Are you interested in receiving Grist training? Interested Submit Thank you for registering! We'll be in touch. Meanwhile, you are welcome to reach out to us with any questions, at support@getgrist.com . Could not submit the form. Try again function formDataToObj(formElem) { const formData = new FormData(formElem); const data = {}; for (const pair of formData.entries()) { if (typeof pair[1] === 'string') { data[pair[0]] = [pair[1]]; } } return data; } const form = document.getElementById('creg-form'); form.addEventListener('submit', (ev) => { ev.preventDefault(); const data = formDataToObj(form); const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } }; window.fetch(form.action, options) .then(resp => { return resp.json() .then(body => { console.log(\"BODY\", body); if (resp.status !== 200) { throw new Error(\"Could not submit the form: \" + (body && body.error || \"unknown error\")); } console.log(\"Form submitted\", body); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-submitted').style.display = 'block'; }) }) .catch(err => { console.warn(\"ERROR\", err); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-error-message').textContent = String(err); document.getElementById('creg-error').style.display = 'block'; }); });","title":"Register to be a Grist consultant"},{"location":"register-as-consultant/#register-to-be-a-grist-consultant","text":".creg-form { line-height: initial; } .form-group .control-label { font-weight: normal; } .creg-button { margin: 24px 0; background-color: #11b683; border: none; color: white; } .creg-button:hover { background-color: #009058; border: none; color: white; } #creg-submitted, #creg-error { display: none; margin-top: 40px; } Are you a freelancer? Do you enjoy building in Grist and organizing data? We\u2019d be glad to support you, and to add you to our list of independent Grist consultants. Please fill out the information below, and we\u2019ll get in touch with you. Your Name: Email: Phone: Company: Website: Your technical background: Excel: None Beginner Intermediate Advanced SQL: None Beginner Intermediate Advanced Python: None Beginner Intermediate Advanced Javascript: None Beginner Intermediate Advanced Grist: None Beginner Intermediate Advanced Are you interested in receiving Grist training? Interested Submit Thank you for registering! We'll be in touch. Meanwhile, you are welcome to reach out to us with any questions, at support@getgrist.com . Could not submit the form. Try again function formDataToObj(formElem) { const formData = new FormData(formElem); const data = {}; for (const pair of formData.entries()) { if (typeof pair[1] === 'string') { data[pair[0]] = [pair[1]]; } } return data; } const form = document.getElementById('creg-form'); form.addEventListener('submit', (ev) => { ev.preventDefault(); const data = formDataToObj(form); const options = { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', } }; window.fetch(form.action, options) .then(resp => { return resp.json() .then(body => { console.log(\"BODY\", body); if (resp.status !== 200) { throw new Error(\"Could not submit the form: \" + (body && body.error || \"unknown error\")); } console.log(\"Form submitted\", body); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-submitted').style.display = 'block'; }) }) .catch(err => { console.warn(\"ERROR\", err); document.getElementById('creg-form').style.display = 'none'; document.getElementById('creg-error-message').textContent = String(err); document.getElementById('creg-error').style.display = 'block'; }); });","title":"Register to be a Grist consultant"},{"location":"rest-api/","text":"Grist API Usage # Grist has an API for manipulating documents, workspaces, and team sites. API Reference shows documentation of all available endpoints. Interactive API Console allows you to make API calls using your Grist login. Authentication # To access the Grist API, you\u2019ll need an API key. An API key is owned by a single user, and has the same permissions as that user. To enable API access for yourself, visit your Profile Settings . You can always find this page by clicking your profile picture or initial on the top right of the screen to open the account menu. Then select the \u201cProfile Settings\u201d option: This shows a dialog with all of your profile setting options. Scroll down to the \u201cAPI\u201d section. Click on the \u201cCreate\u201d button to create an API Key. You can now copy this key for use when making API calls. To be clear, copy the key in your profile settings, not the key in the above screenshot, which isn\u2019t a real one. You can revoke your API key by clicking \u201cRemove\u201d from Profile Settings at any time. You\u2019ll then have the option to create a new one if you wish. To test your api key, try this from the command-line (substituting your api key): curl -H \"Authorization: Bearer Waiting for data...
The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support. Adding a custom widget # To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html Access level # When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget. Invoice example # The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord . Creating a custom widget # As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API. Column mapping # Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping. Widget options # If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen. Custom Widget linking # Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'}); Premade Custom Widgets # All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown. Advanced Charts # The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration. Copy to clipboard # Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Dropbox Embedder # View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template. Grist Video Player # Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget. HTML Viewer # The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar . Image Viewer # View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar ! JupyterLite Notebook # This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install Waiting for data...
The \u201cGrist\u201d parts of this are: Including https://docs.getgrist.com/grist-plugin-api.js to get the Grist API. Calling grist.ready to let Grist know the widget is ready to go. Calling grist.onRecord to subscribe to the current row of the table. After that, everything else is regular HTML/CSS/JS. Once you have data coming in, you can render it any way you like, using React, Vue.js, or your favorite framework. For example, you could render records as a printable invoice , or use some obscure chart format that Grist doesn\u2019t currently support.","title":"Minimal example"},{"location":"widget-custom/#adding-a-custom-widget","text":"To add a custom widget that reads from a table, click on Add New , then Add Widget to Page . Then: For Select Widget choose Custom to get a Custom Widget. For Select Data choose the table you want the widget to read data from. Optionally, choose Select By to control the selected data further (read Linking Page Widgets for the possibilities). The custom widget is initially blank. To configure it, click the three-dots button on the top right of the custom widget, and select \u201cWidget options\u201d. In the CUSTOM settings section where it says Enter Custom URL , put the link to your custom widget. Here is a test widget to simply show table data in JSON: https://gristlabs.github.io/grist-widget/inspect/onRecords.html And here\u2019s one to show the selected row only (make sure \u201cSelect By\u201d is set for the custom widget): https://gristlabs.github.io/grist-widget/inspect/onRecord.html","title":"Adding a custom widget"},{"location":"widget-custom/#access-level","text":"When you put a link to a custom webpage it will be immediately rendered inside the section. Now you have the option to grant that webpage access to data in your document. The following access levels are available: No document access : the webpage is shown in the widget, but it has no access to the Grist document containing the widget. Read selected table : the webpage is shown in the widget, and is given read access to the table the widget is configured to select data from. Full document access : the webpage is shown in the widget, and has full access to read and modify the Grist document containing the widget. The webpage should be owned and controlled by you or someone you trust. With Read selected table permissions, a widget could send the data it accesses to a third party. With Full document access permissions, a widget could send all the document data to a third party, and modify your document in any way. If you are writing your own custom widget you can specify what access level it needs as part of the initial ready message. Possible values are: none , read table and full . This directs Grist to request the desired access level from the user. Your widget will be reloaded with the appropriate access level if the user approves the request. If you wish to get notified of the access level, you can subscribe to the onOptions event that is sent to the widget after it tells Grist it is ready: grist.onOptions(function(options, interaction) { console.log(interaction.access_level); }); For now, just skip the options parameter (it will be described in Widget options section). The current access level is part of the second parameter, which describes how Grist will interact with your widget.","title":"Access level"},{"location":"widget-custom/#invoice-example","text":"The minimal example above showed records as plain JSON, but the widget can get as fancy as you like. Here is an example of showing a record as a printable invoice: You can read the details of how to use this widget in our Invoice preparation example . The invoice widget is hosted at: https://gristlabs.github.io/grist-widget/invoices/ And the source HTML/CSS/JS can be browsed at: https://github.com/gristlabs/grist-widget/tree/master/invoices It uses Vue.js and grist.onRecord .","title":"Invoice example"},{"location":"widget-custom/#creating-a-custom-widget","text":"As you saw, writing a simple widget that uses data from a table is very easy. First, you need to tell Grist that you are ready and then subscribe to one of the available events: onRecord , onRecords or onOptions . grist.ready(); grist.onRecord(function (record) { // Cursor has moved. }); grist.onRecords(function (record) { // Data in the table has changed. }); grist.onOptions(function (options, interaction) { // Configuration has changed. }); Let\u2019s say you want to build a custom widget that will show an image from a URL and optionally a single line of text below as the image title. You will need to read two values from two columns: Link and Title . You could access those columns directly using literal column names in your script. Here is a complete example of widget source code that will do the job: When getting started, this is a good approach, but it has two significant drawbacks. Every time you rename a column, you will also have to change your widget\u2019s source. Moreover, using this widget on a different table or sharing it with your friends can be difficult as column names might be different. To help with this, Grist offers the column mapping API.","title":"Creating a custom widget"},{"location":"widget-custom/#column-mapping","text":"Instead of using column names directly, you can ask the user to pick which column to use as a Link and Title . The list of expected columns can be sent to Grist as part of the ready call: grist.ready({columns: ['Link', 'Title']}); Using this information, in the creator panel, Grist will hide the regular \u201cVisible\u201d columns section and display specialized column pickers. Your widget will receive this mapping configuration as part of onRecord or onRecords event in the second parameter. You can use this configuration to do the mappings yourself or use the mapColumnNames helper function to do it for you. Now, if you rename one of the columns, the widget will still work. You can also use this widget in any other table or share with a friend, as it doesn\u2019t depend on your table structure and can be easily configured. In the configuration used above, we told Grist that all the columns are required, and the user can pick any column even if the column doesn\u2019t contain a text value. To be more precise, we can include more options in the request. For example: grist.ready({columns: [ { name: \"Link\", // What field we will read. title: \"Image link\", // Friendly field name. optional: false, // Is this an optional field. type: \"Text\" // What type of column we expect. description: \"Some text\" // Description of a field. allowMultiple: false // Allows multiple column assignment. } ]}); The optional setting is important for correct operation of the mapColumnNames helper. This helper will return a mapped record only when all required (not optional) columns are picked. By default Grist will allow the user to pick any type of column. To allow only a column of a specific type, you need to set a type property. Here are all valid types: Int ( Integer column ), Numeric ( Numeric column ), Text , Date , DateTime , Bool ( Toggle column ), Choice , ChoiceList , Ref ( Reference column ), RefList ( Reference List ), Attachments . The default value of type is Any , so Grist will allow the user to pick any column type. You can also specify a list of types, for example Date,DateTime . In that case, Grist will allow the user to pick any column that matches one of the types in the list. Use title and description fields to help your users understand what is the purpose of the column. The description will be displayed just below the column name, and the title will be used as a column label. Both are optional and you can put there any text you want. If you need to map multiple columns (for example in a custom chart widget), you can use allowMultiple option. This will allow your users to pick a set of columns that will be returned as list of mapped table column names. The mapColumnNames helper will then return an array of mapped column values in a single field. Suppose the user deletes a column or changes its type so that it will no longer match the type requested by the widget. In that case, Grist will automatically remove this column from the mapping.","title":"Column mapping"},{"location":"widget-custom/#widget-options","text":"If your widget needs to store some options, Grist offers a simple key-value storage API for you to use. Here are some JavaScript code snippets that show how to interact with this API: // Store a simple text value . await grist.setOption('color', '#FF0000'); // Store complex objects as JSON. await grist.setOption('settings', {lines: 10, skipFirst: true}); // Read previously saved option const color = await grist.getOption('color'); // Clear all options. await grist.clearOptions(); // Get and replace all options. await grist.getOptions(); await grist.setOptions({...}); You can experiment with this yourself. Here is a test widget that demonstrates how to use this API: https://gristlabs.github.io/grist-widget/inspect/onOptions.html When your widget saves or edits some options, the icon on top of the section gets highlighted in green. You can either apply those options to the widget or revert that modification. This allows viewers (users with read-only access) or collaborators to configure your widget without overwriting original settings. This behavior should look familiar to you and others, as this works like sorting and filtering on table or card views. Saving current options you will apply them to the widget and make them available to others. Using this menu, you can also clear all options to revert the widget to its initial state. To do this, press the little trash icon and then Save . Grist will also trigger an event, every time the options are changed (or cleared). Here is how you can subscribe to this event. grist.onOptions(function(options, interaction) { if (options) { console.log('Current color', options.color); } else { // No widget options were saved, fallback to default ones. } }); If you are building your own widget, you generally should not read options directly (using grist.widgetApi.getOption() ). A better pattern is to apply them all when they are changed. Using the onOptions handler will make your widget easier to change and understand later. There is one more scenario to cover. Suppose your widget has some kind of custom configuration screen. In that case, you probably need some button or other UI element that the user can use to show it. This additional UI element will likely be rarely used by you or your collaborators, so it doesn\u2019t make sense to show it all the time. To help with this, Grist offers an additional interaction option you can send as part of the ready message: grist.ready({ onEditOptions: function() { // Your custom logic to open the custom configuration screen. } }); This will tell Grist to display an additional button Open configuration in the creator panel and the section menu. When clicked, it will trigger your handler, which you can use to show your own custom configuration screen.","title":"Widget options"},{"location":"widget-custom/#custom-widget-linking","text":"Custom widgets can also be used as a source of linking (see Linking widgets ). All you need to do is inform Grist that your widget supports linking by passing an additional option to the ready call (see Widget API ): grist.ready({ allowSelectBy: true }); This will enable the Select By option in the widget configuration panel. Now you can use your widget to control the cursor position in linked widgets. To do this, you need to call the setCursorPos function: // Inform Grist that the cursor should be moved to the row with id 20. grist.setCursorPos({rowId: 20}); // or inform that your widget is creating a new row. grist.setCursorPos({rowId: 'new'});","title":"Custom Widget linking"},{"location":"widget-custom/#premade-custom-widgets","text":"All premade custom widgets are available in the Custom Widget configuration panel on the right-hand side of the screen under the Custom dropdown.","title":"Premade Custom Widgets"},{"location":"widget-custom/#advanced-charts","text":"The Advanced Charts custom widget gives you more power and flexibility than Grist\u2019s built-in charts, offering a wide variety of chart types as well as increased control over styling and layout. It\u2019s a version of Plotly\u2019s Chart Studio , see their tutorials for more detailed help. You\u2019ll need to set the access level to \u201cFull document access\u201d. Don\u2019t worry, the widget only reads data from the selected table, doesn\u2019t send it to any servers, and doesn\u2019t write or otherwise make changes back to your document. This is what you should see: Click the big blue \u201c+ Trace\u201d button to get started. This will add a panel like the following: Click \u201cScatter\u201d to choose a different chart type such as Bar or Line. Then click the \u201cChoose data\u201d dropdowns to select the columns you want to plot. You can add multiple traces to overlay different plots. Try different panels from the sidebar to customize the chart further. For example, go to Style > Axes > Titles to add a label to each axis. See the chart studio tutorials to learn more. As you customize the widget, remember to regularly click the \u2018Save\u2019 button above the widget to keep your configuration.","title":"Advanced Charts"},{"location":"widget-custom/#copy-to-clipboard","text":"Copy to clipboard copies a value from the specified column of the selected record. When configuring the widget, you will need to select which column you wish to copy data from. Note that you can also copy data from a selected cell by using the keyboard shortcut Ctrl + C on Windows or \u2318 + C on Mac. To paste, use Ctrl + V or \u2318 + V . You can find an example of the copy to clipboard button in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"Copy to clipboard"},{"location":"widget-custom/#dropbox-embedder","text":"View and access files saved to dropbox. To start, add a new column to your table to store your dropbox links. Then, add a new custom widget to the page. Choose the data table that contains the dropbox links and \u2018Select By\u2019 that same table. To configure, select \u2018Dropbox Embedder\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Dropbox Link\u2019, select the column that contains your dropbox links. You can create links to folders or specific files in Dropbox. Click \u2018Share\u2019 then set permissions for the link. You can choose to allow anyone with the link to view or edit. Create, then copy the link. Paste this link into your Dropbox Link column in Grist. Note that users cannot edit directly in the custom widget even if edit permissions are granted. To edit, select the object in the Dropbox Embedder and it will open in a new tab where it can be edited directly in Dropbox. You can check out an example of the Dropbox Embedder in our Hurricane Preparedness template.","title":"Dropbox Embedder"},{"location":"widget-custom/#grist-video-player","text":"Embed videos from online sources like YouTube, Vimeo, Facebook Video, Google Drive and more. To start, add a new column to your table to store your video URLs. Then, add a new custom widget to the page. Choose the data table that contains the video URLs and \u2018Select By\u2019 that same table. To configure, select \u2018Grist Video Player\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018URL\u2019, select the column that contains your video URLs. For most online videos, including YouTube videos and videos stored on Google Drive, you can simply click the \u2018Share\u2019 option and copy the URL. For some other videos, you may see this error: If this happens, you\u2019ll need to take the URL from the Embed code. After clicking the share option on the video, click the option to \u2018Embed\u2019. Then, click to copy the code. The code it gives you will look something like this: Copy the URL that is found between quotes following src . The highlighted portion in the screenshot below is what you would copy for this particular Facebook video. Paste this URL into your URL column in Grist and the video will now appear in the Grist Video Player custom widget.","title":"Grist Video Player"},{"location":"widget-custom/#html-viewer","text":"The HTML viewer displays HTML written in a cell. For text-editing widgets, check out our Markdown and Notepad custom widgets. To start, add a new column to your table. This will be where you add you write HTML. Then, add a new custom widget to the page. Choose the data table that contains the HTML and \u2018Select By\u2019 that same table. To configure, select \u2018HTML Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018HTML\u2019, select the text column that contains your HTML. Your HTML will be viewable in the custom widget. For help on HTML formatting, check out this guide from W3 Schools: HTML Text Formatting You can find an example of the HTML Viewer in our Webinar 7 (Custom Widgets) template. You can also watch a video walkthrough from our Custom Widgets Webinar .","title":"HTML Viewer"},{"location":"widget-custom/#image-viewer","text":"View images from URL. To start, add a new column to your table. This will be where you add the URL for your image. Then, add a new custom widget to the page. Choose the data table that contains the image URL and \u2018Select By\u2019 that same table. To configure, select \u2018Image Viewer\u2019 from the Custom dropdown and allow access to read the selected table. Under \u2018Image URL\u2019, select the column that contains the URLs for your images. To copy the URL for an image, right click on the photo then \u2018Copy image address\u2019. This copies the URL to your clipboard. Paste this URL into your specified column in Grist. For an example of the Image Viewer widget, check out our U.S. National Park Database , and add a park review while you\u2019re there! You can also check out our Meme Generator template for another great example. For a video walkthrough, be sure to watch our Custom Widgets Webinar !","title":"Image Viewer"},{"location":"widget-custom/#jupyterlite-notebook","text":"This widget lets you run custom Python code in JupyterLite , a version of JupyterLab running entirely in the browser. You can use the full custom widget plugin API and access or modify any data in the document (subject to Access Rules), unlocking nearly unlimited possibilities for advanced users. You\u2019ll be presented with a notebook where you can enter and run Python code, e.g: After typing code in a cell, click the play button or press Shift+Enter to run that cell. Unlike formulas, code isn\u2019t saved automatically. You must press the usual \u2018Save\u2019 button above the widget (outside the notebook) to persist the code within your Grist document. On the other hand, changes to settings within the notebook (e.g. keyboard shortcuts) are saved in your browser\u2019s local storage, so they\u2019re not shared with other users of the document. A special object called grist is automatically available to use in Python code, which mirrors many common methods of the usual JS plugin API . Note that many of these methods are asynchronous, so you should use await before calling them. async fetch_selected_table() : returns the data of the table backing the notebook widget. async fetch_selected_record(row_id=None) : returns a record of the table backing the notebook widget. If row_id is specified, returns the record at that row. Otherwise, returns the record at the current cursor position in a widget linked to the notebook widget. async fetch_table(table_id) : returns the data of the specified table. Note that this differs from fetch_selected_table (even for the same table) in several ways: The widget must have full document access. All columns are included, whereas fetch_selected_table excludes columns that are hidden in the widget configuration. All rows are included, whereas fetch_selected_table takes widget filters and \u2018SELECT BY\u2019 into account. The data is not sorted according to the widget\u2019s configuration. The data is fetched from the server, so the method may be slower. The values for reference columns are row IDs of the referenced table, whereas fetch_selected_table returns the values displayed based on the \u2018SHOW COLUMN\u2019 configuration. on_record(callback) : registers a callback function to run when the cursor moves in a widget linked to the notebook widget, i.e. the widget chosen from the \u201cSELECT BY\u201d dropdown in the Data section of the widget configuration. The callback function will be passed the record at the current cursor position. You can also use this as a decorator, i.e. @grist.on_record . on_records(callback) : similar to on_record , but runs when the source data of the widget changes. The callback function will be passed the same data as returned by fetch_selected_table . get_table(table_id) : returns a TableOperations class similar to the interface in the usual JS plugin API for performing CRUD-style operations on a table. See the plugin API documentation for details on the parameters. The class has the following methods: async create(records, parse_strings=True) async update(records, parse_strings=True) async upsert(records, parse_strings=True, add=True, update=True, on_many=\"first\", allow_empty_require=False) async destroy(row_ids) You can also use grist.raw for direct access to the plugin API, e.g. await grist.raw.docApi.fetchTable(table_id) . This may return raw cell values which you can decode with grist.decode_cell_value(value) . You can use many (but not all) third-party libraries in your notebook such as pandas . Many will be installed automatically when they\u2019re imported. Others will require running %pip install
'), )) This tells the formula what to use in place of the variable {Customer_Address} . Printing and Saving # Once your proposal is ready to go, you can print it or save it as a PDF. Click the three-dot icon at the upper-right of the widget then select \u2018Print widget\u2019. From here, you can either select a printer or choose \u2018Save as PDF\u2019 from the \u2018Destination\u2019 dropdown. Setting up multiple forms # You can add more form templates by following the same steps that we have just completed. Add a new template to the Templates table then build out the template using variables containing column IDs for any data that is project-specific. If you have some sections that are the same as another form, copy it over to save yourself the trouble of retyping! Create a dashboard where you can select a project and enter details for this form then preview the form in a custom widget. Don\u2019t forget, you\u2019ll need to add a formula column that combines the new form template with details for the selected project! This formula column is what you\u2019ll select under the \u2018Content\u2019 dropdown while configuring the Markdown Custom Widget .","title":"Proposals & Contracts"},{"location":"examples/2023-07-proposals-contracts/#creating-proposals","text":"If you are keeping business details and contracts in Grist, it can be convenient to generate proposals and contracts right there, alongside those records. You can use the Markdown Custom Widget to create a custom \u2018form\u2019 for Proposals, Contracts, or many other types of documents. This tutorial shows you how to set up a document like this: You can find a finished template here: \ud83d\udcdd Proposals & Contracts Template . If you\u2019d like to add a proposal to an existing document, understanding this tutorial should get you there.","title":"Creating Proposals"},{"location":"examples/2023-07-proposals-contracts/#setting-up-a-project-table","text":"First of all, make a table to record project details by creating an empty document and renaming Table1 to Projects : We\u2019ll create our Proposal template alongside our Projects table. We can insert column IDs as placeholders in our Proposal template that will then be replaced by the cell value for the selected project. For example, in the screenshot below, the value in the Project Name column will replace the variable {Project_Name} in the proposal template on the right. Seeing the available columns while creating our proposal will make it easier to populate those variables.","title":"Setting up a Project table"},{"location":"examples/2023-07-proposals-contracts/#creating-templates","text":"Let\u2019s add a new table, Templates , to the page to store our template data. Add two columns: Name and Template Formatting . Now, let\u2019s add a custom widget beside the table to view our Template Formatting . Click the green \u2018Add New\u2019 button then \u2018Add widget to page\u2019. Under \u2018Select Widget\u2019, select \u2018Custom\u2019 and under \u2018Select Data\u2019, select Templates . Under \u2018Select By\u2019, select Templates again. Configure the custom widget by selecting \u2018Markdown\u2019 from the \u2018Custom\u2019 dropdown. Since we will be editing the template directly in the custom widget, you must allow \u2018Full document access\u2019 under Access Level so the widget can update the Templates table. Under \u2018Content\u2019, select the column Template Formatting . This is the column that will be updated when we make edits within the custom widget. Create a Template in the Templates table by entering a value in the Name column. Then, start editing the template formatting within the custom widget. The widget uses Markdown formatting to format the text. For help on Markdown, click the ? at the top of the widget to view the Markdown Guide . When you click the \u2018save\u2019 icon, formatting from the widget will populate the Template Formatting column. We will exclusively use the custom widget to edit the template formatting so this column can be hidden from the table view. To hide the column, right-click on the column header then \u2018Hide column\u2019. In your template, you\u2019ll have details and text that remain the same across all projects such as formatting, section headers and your own company\u2019s information. That is the information you\u2019ll type directly into the template. You\u2019ll also have information that changes, such as Project Name or Customer Name . We can use variables containing column IDs as placeholders for that dynamic data. Project Name , Customer Name and Customer Address will all change based on the selected Project. So, this is information we should store in our Projects table. Add the columns Project Name , Customer Name and Customer Address to the Projects table. We can use the column IDs for each of these columns as placeholders in our template with the format {COLUMN_ID} . A column\u2019s ID can be found under the \u2018Table\u2019 tab of the Creator Panel, directly under the Column Label. Finish building out your template to fit your needs. Be sure to add a column to your Projects table for all variable information. Finally, we need to add a formula column that will create our unique proposals. This formula column will combine the template formatting we just created with our project-specifc data. Add a new column to the Projects table with the following formula: # Finds all data associated with this record class Find_Data(dict): def __missing__(self, key): return getattr(rec, key) # Finds the \"Proposal\" template in the Templates table template = Templates.lookupOne(Name=\"Proposal\").Template_Formatting # Formats the template with fields from this table as well as fields from the referenced table template.format_map(Find_Data())","title":"Creating templates"},{"location":"examples/2023-07-proposals-contracts/#setting-up-a-proposal-dashboard","text":"Next, we\u2019ll want to populate our proposal template with actual project data! Start creating a Proposal Dashboard by adding a new page to your document. Click the green \u2018Add New\u2019 button then \u2018Add Page\u2019. Under \u2018Select Widget\u2019, select \u2018Table\u2019 and under \u2018Select Data\u2019, select Projects . You\u2019ll notice that having this information in a table view is a bit busy. A Card widget will help simplify our view. Add a new widget to the page by clicking the green \u2018Add New\u2019 button then \u2018Add widget to page\u2019. Under \u2018Select Widget\u2019, select \u2018Card\u2019 and under \u2018Select Data\u2019, select Projects . Under \u2018Select By\u2019, select Projects and add to page. Your dashboard should look similar to the screenshot below. Now that we have all of our Project details in a Card view, we can hide them from our table view. Under the \u2018Table\u2019 tab of the Creator Panel, select all columns except Project Name and Customer Name then click the green \u2018Hide Columns\u2019 button. Keeping most project details in the Card widget, rather than the Table widget, simplifies our dashboard. You can easily see all projects in the Table widget, and when you want to see details for a specific project, select the project and the Card widget will update to show you project details. Finally, we\u2019ll want to add a view of our project-specific proposal. Add a new custom widget to the page by clicking the green \u2018Add New\u2019 button then \u2018Add widget to page\u2019. Under \u2018Select Widget\u2019, select \u2018Custom\u2019 and under \u2018Select Data\u2019, select Projects . Under \u2018Select By\u2019, select Projects and add to page. Configure the custom widget by selecting \u2018Markdown\u2019 from the \u2018Custom\u2019 dropdown. You must allow \u2018Full document access\u2019. Under \u2018Content\u2019, select the column Proposal . This is the formula column that combines our template formatting with our project-specific data. Customize your layout by rearranging and resizing widgets. Add project details for a new project and see how your proposal updates to display the newly added data.","title":"Setting up a proposal dashboard"},{"location":"examples/2023-07-proposals-contracts/#entering-customer-information","text":"Now, let\u2019s make two useful changes to the Project set-up: Put customer information in a separate table, so we don\u2019t have to re-enter their address every time we create a proposal for them (and we can import the addresses in bulk). Update the formula in the Proposal column of the Projects table to look for information in another table. First, create a new table called Customers for customer-specific information like Name and Address . Some of this data is included in our Projects data set. To avoid duplicating data, we need to update our Customer Name and Customer Address columns to pull from our Customers table. On our Proposals Dashboard page, select the Customer Name field then update the column type to Reference . Confirm that \u2018Data from Table\u2019 is set to Customers and \u2018Show Column\u2019 is Name . Next, we need to update the Customer Address field to pull the address for the customer listed in the Customer Name column. Update the Customer Address column to use the following formula: $Customer_Name.Address This formula uses our reference column, Customer , along with dot notation , to pull the value from the Address column of the referenced table. When you take a look at a proposal for an existing project, you\u2019ll notice that the Customer Name no longer populates. This is because of the way reference columns store data. Although under \u2018Show Column\u2019, we chose to see the value from the Name column of the referenced table, reference columns actually store a record\u2019s ID. That is what we are seeing now in the proposal. We can modify our formula in the Proposal column to look for data in other tables. In the Projects table, update the formula in the Proposals column to the following: # Finds all data associated with this record class Find_Data(dict): def __missing__(self, key): return getattr(rec, key) # Finds the \"Proposal\" template in the Templates table template = Templates.lookupOne(Name=\"Proposal\").Template_Formatting # Formats the template with fields from this table as well as fields from the referenced table template.format_map(Find_Data( Customer_Name = $Customer_Name.Name, )) In the last portion of the formula, we can specify variables that pull from other tables. Customer_Name = $Customer_Name.Name is for our reference column, Customer Name . It uses dot notation to specify what data to pull from the referenced table. Note: Customer Address The Customer Address column can be deleted from the Projects table completely. This data is already stored in the Customers table and our Customer Name column is a reference column pointing to this table. We can use this reference column to pull any other information from the Customers table to include in our proposal. If you choose to delete Customer Address from the Projects table, update the last section of formula to the following: template.format_map(Find_data( Customer_Name = $Customer_Name.Name, Customer_Address = $Customer_Name.Address.replace('\\n', '
'), )) This tells the formula what to use in place of the variable {Customer_Address} .","title":"Entering customer information"},{"location":"examples/2023-07-proposals-contracts/#printing-and-saving","text":"Once your proposal is ready to go, you can print it or save it as a PDF. Click the three-dot icon at the upper-right of the widget then select \u2018Print widget\u2019. From here, you can either select a printer or choose \u2018Save as PDF\u2019 from the \u2018Destination\u2019 dropdown.","title":"Printing and Saving"},{"location":"examples/2023-07-proposals-contracts/#setting-up-multiple-forms","text":"You can add more form templates by following the same steps that we have just completed. Add a new template to the Templates table then build out the template using variables containing column IDs for any data that is project-specific. If you have some sections that are the same as another form, copy it over to save yourself the trouble of retyping! Create a dashboard where you can select a project and enter details for this form then preview the form in a custom widget. Don\u2019t forget, you\u2019ll need to add a formula column that combines the new form template with details for the selected project! This formula column is what you\u2019ll select under the \u2018Content\u2019 dropdown while configuring the Markdown Custom Widget .","title":"Setting up multiple forms"},{"location":"install/aws-marketplace/","text":"AWS Marketplace # Grist on the AWS Marketplace has what you need to run a self-hosted Grist instance with minimal setup, and is based on grist-omnibus . Below are the complete configuration steps, including authentication via OpenID. First run setup # After deploying the instance, Grist should be instantly available through the HTTP protocol on an autogenerated domain such as ec2-3-94-254-105.compute-1.amazonaws.com (labeled Public IPv4 DNS by AWS). Default credentials: email: admin@example.getgrist.com password: [instance-id]* * Instance ID can be found on the EC2 page in the AWS Console: How to log in to the Grist instance # During deployment, you should have been asked about creating or using key pairs. You can use this pair to log in via SSH from your terminal/bash. The default user for the Grist EC2 instance is named \u201cubuntu\u201d, and you can log in to ubuntu@[ec2-instance-public-ip] . Note: You need to use the *.pem file you received while generating key pairs on AWS. Details about connecting via SSH can be found in the following places: Windows: https://learn.microsoft.com/en-us/windows/terminal/tutorials/ssh Linux: https://www.ssh.com/academy/ssh/command macOS: https://www.servermania.com/kb/articles/ssh-mac If you don\u2019t want to connect via SSH, AWS provides the option to connect from within the AWS console using the \u201cConnect\u201d button: Custom domain and SSL setup for HTTPS access # Custom domains are required for secure access to Grist. If you already have an SSL certificate, you can use your own (as described in the grist-omnibus README ). If not, Grist can generate a certificate from Let\u2019s Encrypt. For that, a valid domain and email must be configured: Point the domain to the IP address of the Grist EC2 instance. If you don\u2019t use the Elastic IP service , the instance can have a different public IPv4 address each time it\u2019s started. Log in to the Grist EC2 instance. Set the URL parameter in the grist/gristParameters file. You need administrator privileges to perform this action, so you can open an editor by running sudo nano grist/gristParameters . Run the restartGrist script with sudo ~/grist/restartGrist . Once the above steps are completed, you should be able to access Grist on your custom domain. Authentication setup # We support Google or Microsoft as OpenID providers. For configuring other authentication providers, please refer to the dex documentation . To configure Grist authentication with Google or Microsoft, you must have an application registered with the corresponding provider: Microsoft: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings Google: https://support.google.com/cloud/answer/6158849?hl=en Once you have your client ID and secret, you\u2019ll need to pass them to the gristParameters file inside the Grist EC2 instance: Log in to the Grist EC2 instance. Open ~/grist/gristParameters . Update the CLIENT_ID and CLIENT_SECRET sections for the relevant provider(s). If you\u2019re using only one provider, leave the second section commented out. Update ADMIN_EMAIL in the same file. It should correspond to the email you will use to log in via your authentication provider. For example: ADMIN_EMAIL=frank@your-organization.com If you want to change your team\u2019s name, update TEAM_NAME in the same file. Run restartGrist with the clean flag using sudo ~/grist/restartGrist clean to clear old login data. Important: This will delete all Grist documents! Once the above has been configured, you should be able to log in with your Google/Microsoft credentials. Running Grist in a separate VPC # grist-omnibus is designed to work on each account-default VPC. To make it run on a custom VPC, you\u2019ll need to properly configure all VPC elements. For more information on this configuration, read here . To run Grist on a VPC, the following must be properly set up: Assigning a public DNS name to the Grist EC2 instance is allowed. The VPC can be accessed from the internet (allowing internet gateway and routing tables to handle traffic). A security group connection from ports 22 (SSH for configuration), 80 (HTTP connection) and 433 (HTTPS connection) is allowed. Updating grist-omnibus # The packaged version of grist-omnibus will auto-update before each launch. To update grist-omnibus manually, restart the Grist EC2 instance or log in via SSH and call sudo ~/grist/restartGrist . There are currently no plans to support the Grist AWS Marketplace environment outside of grist-omnibus . Other important information # The Grist EC2 instance should have the \u201cPersistent store\u201d option checked. Grist stores all the data in the ~/grist-persist directory. Deleting this folder will result in a loss of all data from all documents. Do not delete ~/grist-persist/acme.json , as it contains a private key from Let\u2019s Encrypt. Deleting it too often can result in Let\u2019s Encrypt denying issuing further certificates from your domain.","title":"AWS Marketplace"},{"location":"install/aws-marketplace/#aws-marketplace","text":"Grist on the AWS Marketplace has what you need to run a self-hosted Grist instance with minimal setup, and is based on grist-omnibus . Below are the complete configuration steps, including authentication via OpenID.","title":"AWS Marketplace"},{"location":"install/aws-marketplace/#first-run-setup","text":"After deploying the instance, Grist should be instantly available through the HTTP protocol on an autogenerated domain such as ec2-3-94-254-105.compute-1.amazonaws.com (labeled Public IPv4 DNS by AWS). Default credentials: email: admin@example.getgrist.com password: [instance-id]* * Instance ID can be found on the EC2 page in the AWS Console:","title":"First run setup"},{"location":"install/aws-marketplace/#how-to-log-in-to-the-grist-instance","text":"During deployment, you should have been asked about creating or using key pairs. You can use this pair to log in via SSH from your terminal/bash. The default user for the Grist EC2 instance is named \u201cubuntu\u201d, and you can log in to ubuntu@[ec2-instance-public-ip] . Note: You need to use the *.pem file you received while generating key pairs on AWS. Details about connecting via SSH can be found in the following places: Windows: https://learn.microsoft.com/en-us/windows/terminal/tutorials/ssh Linux: https://www.ssh.com/academy/ssh/command macOS: https://www.servermania.com/kb/articles/ssh-mac If you don\u2019t want to connect via SSH, AWS provides the option to connect from within the AWS console using the \u201cConnect\u201d button:","title":"How to log in to the Grist instance"},{"location":"install/aws-marketplace/#custom-domain-and-ssl-setup-for-https-access","text":"Custom domains are required for secure access to Grist. If you already have an SSL certificate, you can use your own (as described in the grist-omnibus README ). If not, Grist can generate a certificate from Let\u2019s Encrypt. For that, a valid domain and email must be configured: Point the domain to the IP address of the Grist EC2 instance. If you don\u2019t use the Elastic IP service , the instance can have a different public IPv4 address each time it\u2019s started. Log in to the Grist EC2 instance. Set the URL parameter in the grist/gristParameters file. You need administrator privileges to perform this action, so you can open an editor by running sudo nano grist/gristParameters . Run the restartGrist script with sudo ~/grist/restartGrist . Once the above steps are completed, you should be able to access Grist on your custom domain.","title":"Custom domain and SSL setup for HTTPS access"},{"location":"install/aws-marketplace/#authentication-setup","text":"We support Google or Microsoft as OpenID providers. For configuring other authentication providers, please refer to the dex documentation . To configure Grist authentication with Google or Microsoft, you must have an application registered with the corresponding provider: Microsoft: https://learn.microsoft.com/en-us/power-pages/security/authentication/openid-settings Google: https://support.google.com/cloud/answer/6158849?hl=en Once you have your client ID and secret, you\u2019ll need to pass them to the gristParameters file inside the Grist EC2 instance: Log in to the Grist EC2 instance. Open ~/grist/gristParameters . Update the CLIENT_ID and CLIENT_SECRET sections for the relevant provider(s). If you\u2019re using only one provider, leave the second section commented out. Update ADMIN_EMAIL in the same file. It should correspond to the email you will use to log in via your authentication provider. For example: ADMIN_EMAIL=frank@your-organization.com If you want to change your team\u2019s name, update TEAM_NAME in the same file. Run restartGrist with the clean flag using sudo ~/grist/restartGrist clean to clear old login data. Important: This will delete all Grist documents! Once the above has been configured, you should be able to log in with your Google/Microsoft credentials.","title":"Authentication setup"},{"location":"install/aws-marketplace/#running-grist-in-a-separate-vpc","text":"grist-omnibus is designed to work on each account-default VPC. To make it run on a custom VPC, you\u2019ll need to properly configure all VPC elements. For more information on this configuration, read here . To run Grist on a VPC, the following must be properly set up: Assigning a public DNS name to the Grist EC2 instance is allowed. The VPC can be accessed from the internet (allowing internet gateway and routing tables to handle traffic). A security group connection from ports 22 (SSH for configuration), 80 (HTTP connection) and 433 (HTTPS connection) is allowed.","title":"Running Grist in a separate VPC"},{"location":"install/aws-marketplace/#updating-grist-omnibus","text":"The packaged version of grist-omnibus will auto-update before each launch. To update grist-omnibus manually, restart the Grist EC2 instance or log in via SSH and call sudo ~/grist/restartGrist . There are currently no plans to support the Grist AWS Marketplace environment outside of grist-omnibus .","title":"Updating grist-omnibus"},{"location":"install/aws-marketplace/#other-important-information","text":"The Grist EC2 instance should have the \u201cPersistent store\u201d option checked. Grist stores all the data in the ~/grist-persist directory. Deleting this folder will result in a loss of all data from all documents. Do not delete ~/grist-persist/acme.json , as it contains a private key from Let\u2019s Encrypt. Deleting it too often can result in Let\u2019s Encrypt denying issuing further certificates from your domain.","title":"Other important information"},{"location":"install/cloud-storage/","text":"Cloud Storage # This feature allows automatic syncing of Grist documents and document versions to S3-compatible stores such as MinIO (or AWS S3 itself). Grist Enterprise has native support for Azure storage accounts, and for AWS S3 using AWS\u2019s official client. It is advisable to have Redis enabled when using cloud storage, since this is the best-tested configuration. Enabling snapshotting results in a big change in how documents are stored, and is best done prior to creating documents. Back up your work before changing this configuration. S3-compatible stores via MinIO client # Turn this on by setting the following environment variables: Set GRIST_DOCS_MINIO_ACCESS_KEY and GRIST_DOCS_MINIO_SECRET_KEY . Set GRIST_DOCS_MINIO_BUCKET to the name of a versioned bucket you have created. It is important that the bucket have versioning enabled. Set GRIST_DOCS_MINIO_ENDPOINT to the appropriate hostname - no protocol, no port. (Optional) Set GRIST_DOCS_MINIO_USE_SSL to 1 to use https protocol (default) or 0 for http . (Optional) Set GRIST_DOCS_MINIO_PORT to the port to use, if the default for the protocol (80/443) isn\u2019t right. If using AWS S3, the endpoint to use is s3.amazonaws.com , and there\u2019s no need to set a port number or SSL flag. The access and secret keys are your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY . Make sure to create a versioned bucket in advance. Azure # For Azure: Create a storage account in the Azure portal. For the storage account\u2019s blob service, make sure that versioning is enabled. Get a connection string from the storage account\u2019s Access Keys section. It may look something like DefaultEndpointsProtocol=https;AccountName=... . Place the connection string in an environment variable called AZURE_STORAGE_CONNECTION_STRING . Set the name of an Azure storage container in an environment variable called GRIST_AZURE_CONTAINER . An example of a container name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_AZURE_PREFIX . S3 with native AWS client # For S3: Set the name of the S3 bucket in an environment variable called GRIST_DOCS_S3_BUCKET . An example of a bucket name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_DOCS_S3_PREFIX . Arrange for access using AWS\u2019s many options; if nothing else, you can set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables. Usage once configured # Once the external storage configuration is in place, start Grist as normal for self-managed Grist. Upon startup, there should be a line like: info: == grist.externalStorage.[s3|azure|minio].active: true All documents will be read from and saved to the corresponding S3 bucket or Azure container. Configuration is simplest on a fresh Grist install without any preexisting Grist documents. Once up and running, it is a good idea to configure the storage account\u2019s \u201clifecycle management\u201d to place any bounds you want on how long versions are retained. Grist has no requirements here, this is strictly to your taste.","title":"Cloud storage"},{"location":"install/cloud-storage/#cloud-storage","text":"This feature allows automatic syncing of Grist documents and document versions to S3-compatible stores such as MinIO (or AWS S3 itself). Grist Enterprise has native support for Azure storage accounts, and for AWS S3 using AWS\u2019s official client. It is advisable to have Redis enabled when using cloud storage, since this is the best-tested configuration. Enabling snapshotting results in a big change in how documents are stored, and is best done prior to creating documents. Back up your work before changing this configuration.","title":"Cloud Storage"},{"location":"install/cloud-storage/#s3-compatible-stores-via-minio-client","text":"Turn this on by setting the following environment variables: Set GRIST_DOCS_MINIO_ACCESS_KEY and GRIST_DOCS_MINIO_SECRET_KEY . Set GRIST_DOCS_MINIO_BUCKET to the name of a versioned bucket you have created. It is important that the bucket have versioning enabled. Set GRIST_DOCS_MINIO_ENDPOINT to the appropriate hostname - no protocol, no port. (Optional) Set GRIST_DOCS_MINIO_USE_SSL to 1 to use https protocol (default) or 0 for http . (Optional) Set GRIST_DOCS_MINIO_PORT to the port to use, if the default for the protocol (80/443) isn\u2019t right. If using AWS S3, the endpoint to use is s3.amazonaws.com , and there\u2019s no need to set a port number or SSL flag. The access and secret keys are your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY . Make sure to create a versioned bucket in advance.","title":"S3-compatible stores via MinIO client"},{"location":"install/cloud-storage/#azure","text":"For Azure: Create a storage account in the Azure portal. For the storage account\u2019s blob service, make sure that versioning is enabled. Get a connection string from the storage account\u2019s Access Keys section. It may look something like DefaultEndpointsProtocol=https;AccountName=... . Place the connection string in an environment variable called AZURE_STORAGE_CONNECTION_STRING . Set the name of an Azure storage container in an environment variable called GRIST_AZURE_CONTAINER . An example of a container name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_AZURE_PREFIX .","title":"Azure"},{"location":"install/cloud-storage/#s3-with-native-aws-client","text":"For S3: Set the name of the S3 bucket in an environment variable called GRIST_DOCS_S3_BUCKET . An example of a bucket name is my-grist-docs . Set a prefix such as v1/ in an environment variable called GRIST_DOCS_S3_PREFIX . Arrange for access using AWS\u2019s many options; if nothing else, you can set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY variables.","title":"S3 with native AWS client"},{"location":"install/cloud-storage/#usage-once-configured","text":"Once the external storage configuration is in place, start Grist as normal for self-managed Grist. Upon startup, there should be a line like: info: == grist.externalStorage.[s3|azure|minio].active: true All documents will be read from and saved to the corresponding S3 bucket or Azure container. Configuration is simplest on a fresh Grist install without any preexisting Grist documents. Once up and running, it is a good idea to configure the storage account\u2019s \u201clifecycle management\u201d to place any bounds you want on how long versions are retained. Grist has no requirements here, this is strictly to your taste.","title":"Usage once configured"},{"location":"install/example-docker-nginx/","text":"Example using Docker and NGINX # This Example is originally authored by Akito . # This is a complete Docker setup example for Grist. The following docker-compose.yml files are needed. You will need to adjust the environment variables to your needs. Requirements # You need to have the most recent Docker distribution including the docker compose extension installed. To prepare the host environment, create an empty directory, and within it do: sudo -u \"$(id -un 1000):$(id -un 1000)\" mkdir -p ./config/nginx/site-confs ./data ./database/data NGINX Reverse Proxy with automatic HTTPS # For automatic HTTPS to work, you first need to setup proper DNS entries for the server you are running this reverse proxy on. This reverse proxy is decoupled from Grist, in a separate docker-compose.yml , so you may conveniently provide additional backends to which it can route traffic - for example, Authelia for authentication. This setup uses SWAG, a Docker image that bundles the NGINX reverse proxy with useful services including TLS certificate generation and renewal. This is the docker-compose.yml file managing the NGINX instance. version: \"3.9\" services: letsencrypt: image: lscr.io/linuxserver/swag # NGINX with automatic HTTPS container_name: nginx-letsencrypt-master network_mode: \"host\" environment: - PUID=1000 # Optional Change - PGID=1000 # Optional Change - TZ=Europe/London # Change! - URL=mydomain.eu # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - SUBDOMAINS=grist,webhook.grist # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - VALIDATION=http - EMAIL=admin@mydomain.eu # Change! - ONLY_SUBDOMAINS=true - STAGING=false # Enable if testing! volumes: - ./config:/config restart: unless-stopped NGINX Configuration # The following configuration is to be placed in ./config/nginx/site-confs/grist.conf , to make the NGINX instance route to Grist properly. server { listen 443 ssl http2; listen [::]:443 ssl http2; # Adjust to your needs! server_name grist.mydomain.eu webhook.grist.mydomain.eu; # enable subfolder method reverse proxy confs include /config/nginx/proxy-confs/*.subfolder.conf; # enable for ldap auth (requires ldap-location.conf in the location block) #include /config/nginx/ldap-server.conf; # enable for Authelia (requires authelia-location.conf in the location block) #include /config/nginx/authelia-server.conf; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } } Grist # This is the docker-compose.yml for the Grist backend. It contains the Grist app deployment, which is accompanied by a PostgreSQL database. # https://github.com/gristlabs/grist-core#using-grist version: \"3.9\" services: grist: image: gristlabs/grist:1.0.8 # Change! --> https://hub.docker.com/r/gristlabs/grist/tags container_name: grist user: \"1000\" # Optional Change env_file: - ./grist.env volumes: - ./data:/persist ports: - 127.0.0.1:3000:8080 depends_on: - database database: image: postgres:15-alpine container_name: grist_db user: \"1000\" # Optional Change env_file: - ./grist_db.env volumes: - ./database/data:/var/lib/postgresql/data Environment # The following .env files must be located in the same folder as the Grist docker-compose.yml . grist.env # # https://github.com/gristlabs/grist-core#environment-variables PORT=8080 APP_HOME_URL=https://grist.mydomain.eu GRIST_ALLOWED_HOSTS=webhook.grist.mydomain.eu # Replace with webhook target domains GRIST_DOMAIN=grist.mydomain.eu GRIST_SINGLE_ORG=myorg GRIST_HIDE_UI_ELEMENTS=billing GRIST_LIST_PUBLIC_SITES=false GRIST_MAX_UPLOAD_ATTACHMENT_MB=10 GRIST_MAX_UPLOAD_IMPORT_MB=300 GRIST_ORG_IN_PATH=false GRIST_PAGE_TITLE_SUFFIX=_blank GRIST_FORCE_LOGIN=true GRIST_SUPPORT_ANON=false GRIST_THROTTLE_CPU=true GRIST_SANDBOX_FLAVOR=gvisor PYTHON_VERSION=3 PYTHON_VERSION_ON_CREATION=3 # Database TYPEORM_DATABASE=grist TYPEORM_USERNAME=grist TYPEORM_HOST=grist_db TYPEORM_LOGGING=false TYPEORM_PASSWORD=mysupersecretpassword CHANGE THIS!!!! TYPEORM_PORT=5432 TYPEORM_TYPE=postgres grist_db.env # # https://hub.docker.com/_/postgres POSTGRES_DB=grist POSTGRES_USER=grist POSTGRES_PASSWORD=mysupersecretpassword CHANGE THIS!!!!","title":"Example using Docker and NGINX"},{"location":"install/example-docker-nginx/#example-using-docker-and-nginx","text":"","title":"Example using Docker and NGINX"},{"location":"install/example-docker-nginx/#this-example-is-originally-authored-by-akito","text":"This is a complete Docker setup example for Grist. The following docker-compose.yml files are needed. You will need to adjust the environment variables to your needs.","title":"This Example is originally authored by Akito."},{"location":"install/example-docker-nginx/#requirements","text":"You need to have the most recent Docker distribution including the docker compose extension installed. To prepare the host environment, create an empty directory, and within it do: sudo -u \"$(id -un 1000):$(id -un 1000)\" mkdir -p ./config/nginx/site-confs ./data ./database/data","title":"Requirements"},{"location":"install/example-docker-nginx/#nginx-reverse-proxy-with-automatic-https","text":"For automatic HTTPS to work, you first need to setup proper DNS entries for the server you are running this reverse proxy on. This reverse proxy is decoupled from Grist, in a separate docker-compose.yml , so you may conveniently provide additional backends to which it can route traffic - for example, Authelia for authentication. This setup uses SWAG, a Docker image that bundles the NGINX reverse proxy with useful services including TLS certificate generation and renewal. This is the docker-compose.yml file managing the NGINX instance. version: \"3.9\" services: letsencrypt: image: lscr.io/linuxserver/swag # NGINX with automatic HTTPS container_name: nginx-letsencrypt-master network_mode: \"host\" environment: - PUID=1000 # Optional Change - PGID=1000 # Optional Change - TZ=Europe/London # Change! - URL=mydomain.eu # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - SUBDOMAINS=grist,webhook.grist # Change here, in ./config/nginx/site-confs/grist.conf & in .env files! - VALIDATION=http - EMAIL=admin@mydomain.eu # Change! - ONLY_SUBDOMAINS=true - STAGING=false # Enable if testing! volumes: - ./config:/config restart: unless-stopped","title":"NGINX Reverse Proxy with automatic HTTPS"},{"location":"install/example-docker-nginx/#nginx-configuration","text":"The following configuration is to be placed in ./config/nginx/site-confs/grist.conf , to make the NGINX instance route to Grist properly. server { listen 443 ssl http2; listen [::]:443 ssl http2; # Adjust to your needs! server_name grist.mydomain.eu webhook.grist.mydomain.eu; # enable subfolder method reverse proxy confs include /config/nginx/proxy-confs/*.subfolder.conf; # enable for ldap auth (requires ldap-location.conf in the location block) #include /config/nginx/ldap-server.conf; # enable for Authelia (requires authelia-location.conf in the location block) #include /config/nginx/authelia-server.conf; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; } }","title":"NGINX Configuration"},{"location":"install/example-docker-nginx/#grist","text":"This is the docker-compose.yml for the Grist backend. It contains the Grist app deployment, which is accompanied by a PostgreSQL database. # https://github.com/gristlabs/grist-core#using-grist version: \"3.9\" services: grist: image: gristlabs/grist:1.0.8 # Change! --> https://hub.docker.com/r/gristlabs/grist/tags container_name: grist user: \"1000\" # Optional Change env_file: - ./grist.env volumes: - ./data:/persist ports: - 127.0.0.1:3000:8080 depends_on: - database database: image: postgres:15-alpine container_name: grist_db user: \"1000\" # Optional Change env_file: - ./grist_db.env volumes: - ./database/data:/var/lib/postgresql/data","title":"Grist"},{"location":"install/example-docker-nginx/#environment","text":"The following .env files must be located in the same folder as the Grist docker-compose.yml .","title":"Environment"},{"location":"install/example-docker-nginx/#gristenv","text":"# https://github.com/gristlabs/grist-core#environment-variables PORT=8080 APP_HOME_URL=https://grist.mydomain.eu GRIST_ALLOWED_HOSTS=webhook.grist.mydomain.eu # Replace with webhook target domains GRIST_DOMAIN=grist.mydomain.eu GRIST_SINGLE_ORG=myorg GRIST_HIDE_UI_ELEMENTS=billing GRIST_LIST_PUBLIC_SITES=false GRIST_MAX_UPLOAD_ATTACHMENT_MB=10 GRIST_MAX_UPLOAD_IMPORT_MB=300 GRIST_ORG_IN_PATH=false GRIST_PAGE_TITLE_SUFFIX=_blank GRIST_FORCE_LOGIN=true GRIST_SUPPORT_ANON=false GRIST_THROTTLE_CPU=true GRIST_SANDBOX_FLAVOR=gvisor PYTHON_VERSION=3 PYTHON_VERSION_ON_CREATION=3 # Database TYPEORM_DATABASE=grist TYPEORM_USERNAME=grist TYPEORM_HOST=grist_db TYPEORM_LOGGING=false TYPEORM_PASSWORD=mysupersecretpassword CHANGE THIS!!!! TYPEORM_PORT=5432 TYPEORM_TYPE=postgres","title":"grist.env"},{"location":"install/example-docker-nginx/#grist_dbenv","text":"# https://hub.docker.com/_/postgres POSTGRES_DB=grist POSTGRES_USER=grist POSTGRES_PASSWORD=mysupersecretpassword CHANGE THIS!!!!","title":"grist_db.env"},{"location":"install/forwarded-headers/","text":"Forwarded Headers # You may have a middleware that does authentication and then passes identity on to web applications in a header. If you do, then Grist can be configured to respect that header. Warning The redirection logic for authentication using forwarded headers currently assumes a single team site configuration, and may misbehave for multi-site configurations. To make this work, here is what you\u2019ll need to do: Set GRIST_FORWARD_AUTH_HEADER to a header that will contain authorized user emails, say x-forwarded-user . This needs to match what your middleware will set. Make sure the /auth/login path is handled by your middleware before reaching Grist. Set GRIST_FORWARD_AUTH_LOGOUT_PATH to a path that will trigger a logout for your middleware (for example, /_oauth/logout ). Make sure that the logout path is handled by your middleware! If you want to allow anonymous access in some cases, make sure all other Grist paths are free of your middleware. Grist will trigger the middleware (by redirecting to /auth/login ) as needed. It\u2019s a good idea to strip GRIST_FORWARD_AUTH_HEADER from outside requests on all paths that aren\u2019t handled by your middleware. Your middleware may allow you to specify where to forward the user to after logging out. That should be /signed-out on the Grist site. Example: traefik-forward-auth # traefik-forward-auth is \u201cA minimal forward authentication service that provides OAuth/SSO login and authentication for the traefik reverse proxy/load balancer.\u201d The GRIST_FORWARD_AUTH_HEADER should be X-Forwarded-User , and this should be set in the authResponseHeaders settings for traefik. The GRIST_FORWARD_AUTH_LOGOUT_PATH should be /_oauth/logout , unless you have changed the default url-path setting for traefik-forward-auth. LOGOUT_REDIRECT for traefik-forward-auth should be https://