# Task  This page documents the `task` model. Use the [Internal API](https://docs.forgelivo.com/api/example-app/development/internal-api) when you need low-level reads and writes that skip action code. ## Data shape  Forgelivo's database stores task records by storing and retrieving each of the fields defined on the model in the Forgelivo Editor to a managed database. Forgelivo has generated a GraphQL type matching the configured fields for task: ```ts export type Task = { __typename: 'Task'; /** The globally unique, unchanging identifier for this record. Assigned and managed by Gadget. */ id: string; /** The time at which this record was first created. Set once upon record creation and never changed. Managed by Gadget. */ createdAt: Date; /** The time at which this record was last changed. Set each time the record is successfully acted upon by an action. Managed by Gadget. */ updatedAt: Date; /** The current state this record is in. Changed by invoking actions. Managed by Gadget. */ state: (string | { [key: string]: Scalars['RecordState'] }); description: (RichText | null); owner: (User | null); ownerId: string; title: string; /** Get all the fields for this record. Useful for not having to list out all the fields you want to retrieve, but slower. */ _all: Record; }; ``` ```graphql type Task { """ The globally unique, unchanging identifier for this record. Assigned and managed by Gadget. """ id: GadgetID! """ The time at which this record was first created. Set once upon record creation and never changed. Managed by Gadget. """ createdAt: DateTime! """ The time at which this record was last changed. Set each time the record is successfully acted upon by an action. Managed by Gadget. """ updatedAt: DateTime! """ The current state this record is in. Changed by invoking actions. Managed by Gadget. """ state: RecordState! description: RichText owner: User ownerId: GadgetID title: String! """ Get all the fields for this record. Useful for not having to list out all the fields you want to retrieve, but slower. """ _all: JSONObject! } ``` You can preview what a real record's shape looks like by fetching it using the  [example-app API playground](https://example-app.gadget.app/api/playground/javascript?environment=development). Any fetched task record will have this same `Task` type, and expose the same data by default, regardless of if it's fetched by ID or as part of a `findMany`. This means you can select any of the record's fields wherever you like in a GraphQL query according to the use case at hand. ## Retrieving one task record  Individual task records can be retrieved using the "find by ID" API endpoint. You can also return only some fields, or extra fields beyond what Forgelivo retrieves by default, using the option. The `findOne` function throws an error if no matching record is found, which you will need to catch and handle. Alternatively, you can use the `maybeFindOne` function, which returns `null` if no record is found, without throwing an error. Similarly, the `useFindOne` React hook returns (but does not throw) an error when no matching record is found, while the `useMaybeFindOne` hook simply returns `null` if no record is found, without also returning an error. ```typescript const taskRecord = await api.task.findOne("123"); // => a string console.log(taskRecord.id); // => a Date object console.log(taskRecord.createdAt); ``` ```tsx const FindOneComponent = () => { const [{data, error, fetching}, refresh] = useFindOne(api.task, "123"); // => a string console.log(data?.id); // => a Date object console.log(data?.createdAt); return <>{JSON.stringify(data)}; }; ``` ```graphql query GetOneTask($id: GadgetID!) { task(id: $id) { __typename id state # ... createdAt description { markdown truncatedHTML } ownerId title updatedAt } } ``` ```json { "id": "123" } ``` ## Retrieving the first of many task records  The first record from a list of records can be retrieved using the "find first" API endpoint. The source list of records can be filtered using the option, sorted using the option, and if search is enabled, searched using the option. No pagination options are available on this endpoint. You can also return only some fields, or extra fields beyond what Forgelivo retrieves by default using the option. The `findFirst` function throws an error if no matching record is found, which you will need to catch and handle. Alternatively, you can use the `maybeFindFirst` function, which returns `null` if no record is found, without throwing an error. Similarly, the `useFindFirst` React hook returns (but does not throw) an error when no matching record is found, while the `useMaybeFindFirst` hook simply returns `null` if no record is found, without also returning an error. ```typescript const taskRecord = await api.task.findFirst(); console.log(taskRecord.id); //=> a string console.log(taskRecord.createdAt); //=> a Date object ``` ```tsx const FindFirstComponent = (props) => { const [{data, error, fetching}, refresh] = useFindFirst(api.task); // => a string console.log(data?.id) // => a Date object console.log(data?.createdAt) return <>{JSON.stringify(data)}; }; ``` ```graphql query FindManyTasks( $first: Int $search: String $sort: [TaskSort!] $filter: [TaskFilter!] ) { tasks(first: $first, search: $search, sort: $sort, filter: $filter) { edges { node { __typename id state # ... createdAt description { markdown truncatedHTML } ownerId title updatedAt } } } } ``` ```json { "first": 1 } ``` ## Retrieving many task records  Pages of task records can be retrieved by using the "find many" API endpoint. The returned records can be filtered using the option, sorted using the option, and if search is enabled, searched using the option. You can also over all the records matching your options using standard Relay-style pagination options. You can also return only some fields, or extra fields beyond what Forgelivo retrieves by default using the option. This GraphQL endpoint returns records in the Relay Connection style (as a list of `edge`s with `node`s and `cursor`s) so they can be . The `tasks` GraphQL endpoint works with any Relay-compatible caching client, or you can use Forgelivo's JS client for pagination with the `findMany` function. ### Find a page of tasks  You can fetch one page of records with the `task.findMany` JS method or the `tasks` GraphQL field. No options are required, and records can be sorted with the `sort` option and filtered with the `filter` optional. The records returned will be implicitly sorted by ID ascending, unless you pass a `sort` option. ```typescript const taskRecords = await api.task.findMany(); // => a number console.log(taskRecords.length); // => a string console.log(taskRecords[0].id); ``` ```tsx const FindManyComponent = () => { const [{data, error, fetching}, refresh] = useFindMany(api.task); if (!data) return null; // => a number console.log(data.length) return <> {data.map(record => JSON.stringify(record))} }; ``` ```graphql query FindManyTasks( $after: String $before: String $first: Int $last: Int $search: String $sort: [TaskSort!] $filter: [TaskFilter!] ) { tasks( after: $after before: $before first: $first last: $last search: $search sort: $sort filter: $filter ) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json {} ``` ### Retrieving related model data  Your app's API is built using GraphQL, which makes it easy to retrieve data from related records through fields like belongs to and has many. To optimize the performance of your API, data from related records is not retrieved by default, but you can change what data is retrieved with the \`select\` option to API calls. For example, you can fetch belongs to data from the record linked by the `owner` field using the option: ```typescript const taskRecord = await api.task.findFirst({ select: { id: true, owner: { id: true, createdAt: true, }, }, }); // => the related record's id console.log(taskRecord.owner?.id); ``` ```tsx const FindRelatedComponent = () => { const [{data, error, fetching}, refresh] = useFindFirst(api.task, { select: { id: true, owner: { id: true, createdAt: true } } }); if (!data) return null; // show the related record's id return <> {data.owner?.id} }; ``` When using an explicit `select` option, be sure to include any fields you need from all models in your query, including the parent model. The default selection no longer applies when you pass `select`. ## Retrieving a single task record by a uniquely identifiable field  After adding a unique validation to a field, you can retrieve a single record by using the finders generated below. If you would like to edit the fields returned or filtering, see the section. ### Retrieving a single task record by id  Individual task records can be retrieved using the `findById` API endpoint, pre-filtered by the unique **id** field. Throws if stored data is not unique. ```typescript const taskRecord = await api.task.findById("some-value"); // => a string console.log(taskRecord.id); ``` ```tsx const FindByComponent = () => { const [{data, error, fetching}, refresh] = useFindBy(api.task.findById, "some-value"); // => a string console.log(data?.id); return <>{JSON.stringify(data)}; } ``` ## Sorting  Records can be sorted in the database to retrieve them in a certain order. Records are always implicitly sorted by ID ascending unless an explicit sort on the `id` field is defined. The GraphQL type `TaskSort` defines which fields can be sorted by. Records can be sorted by multiple different fields and in multiple different directions by passing a list of `TaskSort` instead of just one. Pass the `sort` option to the JS client, or the `sort` variable to a GraphQL query to sort the records returned. ```typescript const taskRecords = await api.task.findMany({ sort: { createdAt: "Descending" } }); ``` ```tsx const [result, refresh] = useFindMany(api.task, { sort: { createdAt: "Descending" } }); const { data, error, fetching } = result; ``` ```graphql query FindManyTasks($sort: [TaskSort!]) { tasks(sort: $sort) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "sort": { "createdAt": "Descending" } } ``` Sort by multiple fields by passing an array of `{ [field]: "Ascending" | "Descending" }` objects. ```typescript const taskRecords = await api.task.findMany({ sort: [{ id: "Descending" }, { createdAt: "Ascending" }], }); ``` ```tsx const [result, refresh] = useFindMany(api.task, { sort: [ { id: "Descending" }, { createdAt: "Ascending" } ] }); const { data, error, fetching } = result; ``` ```graphql query FindManyTasks($sort: [TaskSort!]) { tasks(sort: $sort) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "sort": [{ "id": "Descending" }, { "createdAt": "Ascending" }] } ``` ### Available fields for sorting  You can sort `task` records by the following fields: | Field | Sort type | | --- | --- | | `id` | `Ascending` / `Descending` | | `state` | `Ascending` / `Descending` | | `createdAt` | `Ascending` / `Descending` | | `updatedAt` | `Ascending` / `Descending` | For example, you can sort by the `id` field: ```typescript const taskRecords = await api.task.findMany({ sort: { id: "Descending" }, }); ``` ```tsx const [result, refresh] = useFindMany(api.task, { sort: { id: 'Descending'} }) const { data, error, fetching } = result; ``` ```graphql query FindManyTasks($sort: [TaskSort!]) { tasks(sort: $sort) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "sort": { "id": "Descending" } } ``` Read more about sorting in the [Sorting and Filtering](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering) reference. ## Searching  The `task` model is not being indexed for searching. Enable search indexing in this model's api options. ## Filtering  task records can be filtered to return only the appropriate records. Records can be filtered on any field, including those managed by Forgelivo or fields added by developers. Filters can be combined with sorts, searches and paginated using cursor-based Relay pagination. Filter Tasks by passing the `filter` parameter with a filter object. Filter objects are [nestable](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#combining-filters) boolean conditions expressed as JS objects capturing a key, an operator, and usually a value. ```typescript const yesterday = new Date(Date.now() - 86400000); const taskRecords = await api.task.findMany({ filter: { createdAt: { greaterThan: yesterday } }, }); ``` ```tsx const yesterday = new Date(Date.now() - 86400000); const [result, refresh] = useFindMany(api.task, { filter: { createdAt: { greaterThan: yesterday }} }) const { data, error, fetching } = result; ``` ```graphql query FindManyTasks($filter: [TaskFilter!]) { tasks(filter: $filter) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "filter": { "createdAt": { "greaterThan": "2026-10-28T15:02:30.451Z" } } } ``` ### Available fields for filtering  The GraphQL type `TaskFilter` defines which fields can be filtered on. | Field | Filter type | | --- | --- | | `id` | [IDFilter](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#idfilter) | | `owner` | [BelongsToFilter](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#belongstofilter) | | `state` | [RecordStateFilter](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#recordstatefilter) | | `createdAt` | [DateTimeFilter](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#datetimefilter) | | `updatedAt` | [DateTimeFilter](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#datetimefilter) | Field Filter type For example, we can filter `task` records on the id field: ```typescript const taskRecords = await api.task.findMany({ filter: { id: { isSet: true }, }, }); ``` ```tsx const [result, refresh] = useFindMany(api.task, { filter: { id: {isSet: true} } }) const { data, error, fetching } = result; ``` ```graphql query FindManyTasks($filter: [TaskFilter!]) { tasks(filter: $filter) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "filter": { "id": { "isSet": true } } } ``` Read more about the available filters in the [Sorting and Filtering](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering) reference. ### Combining filters  Records can be filtered by multiple different fields simultaneously. If you want to combine filters using boolean logic, nest them under the `AND`, `OR`, or `NOT` keys of a parent filter. Filters can be [nested deeply](https://docs.forgelivo.com/api/example-app/development/sorting-and-filtering#combining-filters) by passing multiple levels boolean condition filters. You can also pass a list of filters to the `filter` parameter which will be implicitly `AND`ed with one another such that they all need to match for a record to be returned. ```typescript const yesterday = new Date(Date.now() - 86400000); const oneWeekAgo = new Date(Date.now() - 604800000); const taskRecords = await api.task.findMany({ filter: { OR: [ { createdAt: { greaterThan: oneWeekAgo }, }, { updatedAt: { greaterThan: yesterday }, }, ], }, }); ``` ```tsx const yesterday = new Date(Date.now() - 86400000); const oneWeekAgo = new Date(Date.now() - 604800000); const [result, refresh] = useFindMany(api.task, { filter: { OR: [ { createdAt: { greaterThan: oneWeekAgo }, }, { updatedAt: { greaterThan: yesterday }, }, ] } }) const { data, error, fetching } = result; ``` ```graphql query FindManyTasks($filter: [TaskFilter!]) { tasks(filter: $filter) { edges { node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "filter": { "OR": [ { "createdAt": { "greaterThan": "2026-10-22T15:02:30.452Z" } }, { "updatedAt": { "greaterThan": "2026-10-28T15:02:30.452Z" } } ] } } ``` ## Pagination  All Forgelivo record lists, including the top level task finder as well as associations to task, are structured as [GraphQL connections](https://graphql.org/learn/pagination/). GraphQL connections are the de facto standard for querying lists and support cursor-based forward and backward pagination. When querying via GraphQL, you must select the `edges` field and then the `node` field to get the task record. When querying using a Forgelivo API client, the GraphQL queries are generated for you and the records are unwrapped and returned as a `GadgetRecordList` ready for use. task pagination supports the standard GraphQL connection pagination arguments: `first` + `after`, or `last` + `before`. Pagination is done using cursors, which you can retrieve from the `edge.cursor` field or the `pageInfo.startCursor` properties. ```typescript const taskRecords = await api.task.findMany({ first: 25 }); // => no greater than 25 console.log(taskRecords.length); ``` ```tsx const FirstPage = () => { const [{ data, error, fetching }, refresh] = useFindMany(api.task, {first: 25}) if (!data) return null; // => no greater than 25 console.log(data.length); return <> {data.map(record => JSON.stringify(record))} } ``` ```graphql query FindManyTasks($first: Int, $after: String) { tasks(first: $first, after: $after) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "first": 25 } ``` ```typescript const taskRecords = await api.task.findMany({ after: "example-cursor", first: 25 }); ``` ```tsx const SecondPage = (props) => { const [{ data, error, fetching }, refresh] = useFindMany(api.task, {after: "example-cursor", first: 25}); if (!data) return null; // => no greater than 25 console.log(data.length); return <> {data.map(record => JSON.stringify(record))} } ``` ```graphql query FindManyTasks($first: Int, $after: String) { tasks(first: $first, after: $after) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { __typename id state # ... createdAt updatedAt } } } } ``` ```json { "first": 25, "after": "abcdefg" } ``` ### Pagination Limits  Root-level record finders like `tasks` support a maximum page size of 250 records and a default page size of 50 records. The page size is controlled using the `first` or `last` GraphQL field arguments. Related record finders that access lists of records through a has many or has many through field support a maximum page size of 100 records and a default page size of 50 records. #### Get the next or previous page  When using the generated JavaScript API client, including the `api` parameter in a Forgelivo code effect, the record lists returned from findMany calls can be paginated using the `nextPage()` or `previousPage()` option. Both `nextPage()` and `previousPage()` will throw an error if the corresponding `hasNextPage` or `hasPreviousPage` is `false`. ```typescript const taskRecords = await api.task.findMany(); if (taskRecords.hasNextPage) { const nextPage = await taskRecords.nextPage(); } if (taskRecords.hasPreviousPage) { const prevPage = await taskRecords.previousPage(); } ``` When using React and paging through records, you can use cursors to get the previous or next pages of records. This is an example of a React component that pages forward and backward through 2 records at a time for task. ```tsx // your Gadget project's API Client import { api } from "../api"; import { useFindMany } from "@gadgetinc/react"; import { useCallback, useState } from "react"; export default function TestComponent() { // the number of records per page const NUM_ON_PAGE = 2; const [cursor, setCursor] = useState({ first: NUM_ON_PAGE }); // using Gadget React hooks to fetch records of task const [{ data, fetching, error }] = useFindMany(api.task, { ...cursor, }); const getNextPage = useCallback(() => { // use first + after to page forwards setCursor({ first: NUM_ON_PAGE, after: data.endCursor }); }, [data]); const getPreviousPage = useCallback(() => { // use last + before to page backwards setCursor({ last: NUM_ON_PAGE, before: data.startCursor }); }, [data]); return (
{!fetching && data.map((d) =>
{d.id}
)}
); } ``` #### Get all records [Framework v1.5+](https://docs.forgelivo.com/guides/gadget-framework)  If you need to get all available data for task, you can use `iterateAll` or `findAll` to automatically paginate through all pages of data. If you have a large amount of data, this can take a long time. Make sure you need to collect all data at once before writing a pagination loop that reads all records. If you are querying records for display in a UI and cannot display all your records at once, we do not recommend fetching all the data beforehand - instead, use the cursor to read additional data when the user needs it. If you need all data for analytics applications or to collect some statistics on your data, consider options like intermediate models and pre-defined data rollups. ##### `iterateAll` (recommended)  `iterateAll` lazily paginates through all records, yielding one record at a time via `for await`. This is the recommended approach for large datasets because it does not accumulate all records in memory at once. ```typescript for await (const record of api.task.iterateAll({ select: { id: true }, filter: { // add filter conditions, if possible }, })) { // process each record one at a time } ``` `iterateAll` supports `filter`, `sort`, `search`, and `select` options. The `first` option controls the page size, which defaults to 250. The `after` option allows starting iteration from a specific cursor position. Early termination with `break` works naturally and avoids fetching unnecessary pages. ##### `findAll`  `findAll` paginates through all matching records and returns a flat array. This is convenient when you need all records in memory at once. `findAll` can be slow and memory-intensive for large datasets. For better performance with large result sets, use `iterateAll` instead. ```typescript const allRecords = await api.task.findAll({ select: { id: true }, filter: { // add filter conditions, if possible }, }); ``` `findAll` supports the same options as `iterateAll`: `filter`, `sort`, `search`, `select`, `first` for page size, and `after` for the start cursor. ##### Manual pagination  If you need more control over the pagination process, you can manually paginate using `findMany` and cursors: ```typescript // use allRecords to store all records let records = await api.task.findMany({ first: 250, select: { id: true, }, filter: { // add filter conditions, if possible }, }); const allRecords = [...records]; // loop through additional pages to get all protected orders while (records.hasNextPage) { // paginate records = await records.nextPage(); allRecords.push(...records); } ``` ## Selecting fields, and fields of fields  When using the JavaScript client, all of `findOne`, `maybeFindOne`, `findMany`, `findFirst`, `maybeFindFirst`, and various action functions, allow requesting specific fields of a task and its relationships. The `select` option controls which fields are selected in the generated GraphQL query sent to the Forgelivo API. Pass each field you want to select in an object, with `true` as the value for scalar fields, and a nested object of the same shape for nested fields. Forgelivo has a default selection that will retrieve all of the scalar fields for a task. If you don't pass a `select` option to a record finder, this default selection will be used. ```typescript // fetch only the id and createdAt field const taskRecords = await api.task.findMany({ select: { id: true, createdAt: true }, }); ``` ```tsx const SelectionComponent = (props) => { // fetch only the id and createdAt field const [{data, error, fetching}, refresh] = useFindMany(api.task, {select: { id: true, createdAt: true }}); // ... } ``` ## Realtime subscriptions with `live` queries  Each read function in the task runs once and returns a static set of results. There is also a second mode for subscribing to the results of the query as they change over time. By passing `live: true` to any read query or hook, you'll get a stream of results as data changes in the backend, instead of just one result. This is useful for automatically updating your UI when any record is created, updated, or deleted in your backend database. Realtime queries are issued by passing `live: true` to your API call or React hook. For example, you can query and stream changes to the record with id `123`: ```typescript // calling findOne with live: true will return a stream of results as an AsyncIterator, not just one result for await (const record of api.task.findOne("123", { live: true })) { // record will be a GadgetRecord object console.log("new record version", record); } ``` ```tsx export const Record123 = (props) => { const [{data, error, fetching}] = useFindOne(api.task, "123", // the id of the record to fetch { // make this a realtime live query live: true, }); if (!data) return null; return (
Record 123 last updated at {data.updatedAt}
); } ``` You can also subscribe to realtime results for pages of records. For example, we can fetch and render a list of the first ten task records, which will automatically update when backend data changes: ```typescript // calling findMany with live: true will return a stream of results as an AsyncIterator, not just one result for await (const list of api.task.findMany({ live: true, first: 10 })) { // list will be an array of GadgetRecord objects console.log("new query result", list); } ``` ```tsx export const FirstPage = (props) => { const [{data, error, fetching}] = useFindMany(api.task, { // make this a realtime live query live: true, first: 10 }); if (!data) return null; return (
{data.map((record) => (
{record.id}
))}
); } ``` For more information, see the [Realtime Queries](https://docs.forgelivo.com/guides/frontend/realtime-queries) guide. ##### Type Safety  The `select` option is fully type-safe if you're using TypeScript. The returned [`GadgetRecord`](https://docs.forgelivo.com/api/example-app/development/gadget-record) type will have a `` exactly matching the fields and nested fields you selected. This behavior of selecting only some fields is built right into GraphQL. If you want to limit or expand what you retrieve from a GraphQL query, include or exclude those fields in your GraphQL query. For more information on executing GraphQL queries, see [GraphQL](https://docs.forgelivo.com/api/example-app/development/external-api-calls/graphql). ```typescript // fetch the id and createdAt field, and fetch some nested fields from an example relationship field named `someRelatedObject` const taskRecords = await api.task.findMany({ select: { id: true, createdAt: true, someRelatedObject: { id: true, createdAt: true }, }, }); ``` ```tsx const SelectionComponent = (props) => { // fetch the id and createdAt field, and fetch some nested fields from an example relationship field named `someRelatedObject` const [{data, error, fetching}, refresh] = useFindMany(api.task, {select: { id: true, createdAt: true, someRelatedObject: { id: true, createdAt: true } }}); // ... } ``` ## Invoking Actions  task records are changed by invoking Actions. Actions are the things that "do" stuff -- update records, make API calls, call backend code, etc. Actions with a GraphQL API trigger each have one corresponding GraphQL mutation and a corresponding function available in the API client libraries. Nested Actions can also be invoked with the API client, by providing the actions as input to any relationship fields. ### Action Result format  Each API action returns results in the same format that includes a success indicator, errors, and the actual result if the action succeeded. The result is the record that was acted on for a model action, or a list of records for a bulk action, or a JSON blob for Global Actions. Model actions that delete the record don't return the record. The `success` field returns a boolean indicating if the action executed as expected. Any execution errors are returned in the `errors` object, which will always be `null` if `success` is true or contain `ExecutionError` objects if `success` is false. `ExecutionError` objects always have a `message` describing what error prevented the action from succeeding, as well as a `code` attribute that gives a stable, searchable, human-readable error class code for referencing this specific error. Details on each error code can be found in the [Errors](https://docs.forgelivo.com/api/example-app/development/errors) documentation. All `ExecutionError` object types returned by the GraphQL object can be one of many types of error, where some types have extra data that is useful for remedying the error. All error types will always have `message` and `code` properties, but some, like `InvalidRecordError` have extra fields for use by clients. ### Errors when using the generated client  The generated JavaScript client automatically interprets errors from invoking actions and throws JavaScript `Error` instances if the action didn't succeed. The `Error` objects it throws are rich, and expose extra error properties beyond just `message` and `code` if they exist. Errors thrown by the JavaScript client are easiest to catch by using a `try/catch` statement around an `await`, like so: ```typescript import { GadgetOperationError, InvalidRecordError, } from "@gadgetinc/api-client-core"; // must be in an async function to use await` syntax export async function run({ api }) { try { return await api.exampleModel.create({ name: "example record name" }); } catch (error) { if (error instanceof GadgetOperationError) { // a recognized general error has occurred, retry the operation or inspect \error.code\` console.error(error); } else if (error instanceof InvalidRecordError) { // the submitted input data for the action was invalid, inspect the invalid fields which \`InvalidRecordError\` exposes console.error(error.validationErrors); } else { // an unrecognized error occurred like an HTTP connection interrupted error or a syntax error. Re-throw it because it's not clear what to do to fix it throw error; } } } ``` For more information on error codes, consult the [Errors](https://docs.forgelivo.com/api/example-app/development/errors) documentation. ### task create  ##### Input  `create` accepts the following input parameters: ```ts export type CreateTaskInput = { description?: RichTextInput | null; owner?: UserBelongsToInput | null; title?: (Scalars['String'] | null) | null; }; export type CreateTaskArguments = { task?: CreateTaskInput | null; }; ``` ```graphql input CreateTaskInput { description: RichTextInput owner: UserBelongsToInput title: String } input CreateTaskArguments { task: CreateTaskInput } ``` ```typescript const taskRecord = await api.task.create({ description: { markdown: "example _rich_ **text**", }, owner: { _link: "1", }, title: "example value for title", }); ``` ```tsx const ExampleRunCreateComponent = () => { const [{ data, error, fetching }, create] = useAction(api.task.create); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation CreateTask($task: CreateTaskInput) { createTask(task: $task) { success errors { message ... on InvalidRecordError { validationErrors { apiIdentifier message } record model { apiIdentifier } } } task { __typename id state createdAt description { markdown truncatedHTML } ownerId title updatedAt } } } ``` ```json { "task": { "description": { "markdown": "example _rich_ **text**" }, "owner": { "_link": "1" }, "title": "example value for title" } } ``` ##### Output  `create` returns the task. In the JS client, the fields returned can be controlled with the option. In GraphQL, the return format is the , which includes the record if the action was successful. You can include or exclude the fields you need right in the mutation itself. ```graphql type CreateTaskResult implements UpsertTaskResult { success: Boolean! errors: [ExecutionError!] actionRun: String task: Task } ``` ### task update  ##### Input  `update` operates on one task in particular, identified by the `id` variable.`update` accepts the following input parameters: ```ts export type UpdateTaskInput = { description?: RichTextInput | null; owner?: UserBelongsToInput | null; title?: (Scalars['String'] | null) | null; }; export type UpdateTaskArguments = { task?: UpdateTaskInput | null; }; ``` ```graphql input UpdateTaskInput { description: RichTextInput owner: UserBelongsToInput title: String } input UpdateTaskArguments { task: UpdateTaskInput } ``` ```typescript const taskRecord = await api.task.update("1", { description: { markdown: "example _rich_ **text**", }, owner: { _link: "1", }, title: "example value for title", }); ``` ```tsx const ExampleRunUpdateComponent = () => { const [{ data, error, fetching }, update] = useAction(api.task.update); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation UpdateTask($task: UpdateTaskInput, $id: GadgetID!) { updateTask(task: $task, id: $id) { success errors { message ... on InvalidRecordError { validationErrors { apiIdentifier message } record model { apiIdentifier } } } task { __typename id state createdAt description { markdown truncatedHTML } ownerId title updatedAt } } } ``` ```json { "task": { "description": { "markdown": "example _rich_ **text**" }, "owner": { "_link": "1" }, "title": "example value for title" }, "id": "1" } ``` ##### Output  `update` returns the task. In the JS client, the fields returned can be controlled with the option. In GraphQL, the return format is the , which includes the record if the action was successful. You can include or exclude the fields you need right in the mutation itself. ```graphql type UpdateTaskResult implements UpsertTaskResult { success: Boolean! errors: [ExecutionError!] actionRun: String task: Task } ``` ### task delete  The `delete` action destroys the record. ##### Input  `delete` operates on one task in particular, identified by the `id` variable. ```typescript await api.task.delete("1"); ``` ```tsx const ExampleRunDeleteComponent = () => { const [{ data, error, fetching }, _delete] = useAction(api.task.delete); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation DeleteTask($id: GadgetID!) { deleteTask(id: $id) { success errors { message ... on InvalidRecordError { validationErrors { apiIdentifier message } record model { apiIdentifier } } } } } ``` ```json { "id": "1" } ``` ##### Output  `delete` deletes the record, so it returns `void` in the JS client. In GraphQL it returns only the `success` and `errors` from the . ```graphql type DeleteTaskResult { success: Boolean! errors: [ExecutionError!] actionRun: String } ``` ### task upsert  ##### Input  `upsert` accepts the following input parameters: ```ts export type UpsertTaskInput = { id?: (Scalars['GadgetID'] | null) | null; description?: RichTextInput | null; owner?: UserBelongsToInput | null; title?: (Scalars['String'] | null) | null; }; export type UpsertTaskArguments = { /** An array of Strings */ on?: ((Scalars['String'] | null))[]; task?: UpsertTaskInput | null; }; ``` ```graphql input UpsertTaskInput { id: GadgetID description: RichTextInput owner: UserBelongsToInput title: String } input UpsertTaskArguments { """ An array of Strings """ on: [String!] task: UpsertTaskInput } ``` ```typescript const result = await api.task.upsert({ description: { markdown: "example _rich_ **text**", }, id: "1", owner: { _link: "1", }, title: "example value for title", }); ``` ```tsx const ExampleRunUpsertComponent = () => { const [{ data, error, fetching }, upsert] = useAction(api.task.upsert); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation UpsertTask($on: [String!], $task: UpsertTaskInput) { upsertTask(on: $on, task: $task) { success errors { message } } } ``` ```json { "task": { "description": { "markdown": "example _rich_ **text**" }, "id": "1", "owner": { "_link": "1" }, "title": "example value for title" } } ``` ##### Output  `upsert` returns the task. In the JS client, the fields returned can be controlled with the option. In GraphQL, the return format is the , which includes the record if the action was successful. You can include or exclude the fields you need right in the mutation itself. ```graphql interface UpsertTaskResult { success: Boolean! errors: [ExecutionError!] actionRun: String } ``` ## Linking related records  When running actions on the `task` model, you can set the value of any belongs to or has many relationships in the same API call. ### Linking to an existing parent record  For belongs to fields, you can link an `task` record to an already-existing related record by passing `{_link: ""}` as the value for the field in your action. ```javascript const taskRecord = await api.task.update("1", { owner: { _link: "456", }, }); ``` ```react const ExampleRunUpdateComponent = () => { const [{ data, error, fetching }, update] = useAction(api.task.update); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation UpdateTask($task: UpdateTaskInput, $id: GadgetID!) { updateTask(task: $task, id: $id) { success errors { message ... on InvalidRecordError { validationErrors { apiIdentifier message } record model { apiIdentifier } } } task { __typename id state createdAt description { markdown truncatedHTML } ownerId title updatedAt } } } ``` ### Creating a linked child when creating a parent  You can also create a new `task` record when creating a parent record by invoking the child create action as a nested action. Forgelivo will automatically link any records created in nested actions with their parent. ```javascript const userRecord = await api.user.create({ tasks: [ { create: { description: { markdown: "example _rich_ **text**", }, title: "example value for title", }, }, ], }); ``` ```react const ExampleRunCreateComponent = () => { const [{ data, error, fetching }, create] = useAction(api.user.create); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation CreateUser($user: CreateUserInput) { createUser(user: $user) { success errors { message ... on InvalidRecordError { validationErrors { apiIdentifier message } record model { apiIdentifier } } } user { __typename id state createdAt email roles { key name } updatedAt } } } ``` ## Bulk Actions  You can run the same action for an array of inputs all at once with bulk actions. Bulk Actions are executed as a single API call and offer better performance when running the same action on many records. Creates, updates, deletes, and custom actions can all be run in bulk. Bulk Actions repeat the same action each time across a variety of different records. If you want to call different actions, you can't use Bulk Actions and must instead make multiple API calls. If a bulk action group fails on some of the individual records, the remaining records will still be processed, and the list of errors will be returned in the result. Only the records which succeed in executing the action will be returned in the result. ### Bulk task `create`  ##### Input  `bulkCreateTasks` operates on a set of tasks, identified by the `ids` variable. ```typescript const taskRecords = await api.task.bulkCreate([ { description: { markdown: "example _rich_ **text**", }, title: "example value for title", }, { description: { markdown: "example _rich_ **text**", }, title: "example value for title", }, ]); ``` ```tsx const RunActionComponent = () => { const [{ data, error, fetching }, bulkCreate] = useBulkAction(api.task.bulkCreate); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation BulkCreateTasks($inputs: [BulkCreateTasksInput!]!) { bulkCreateTasks(inputs: $inputs) { success errors { message } tasks { id state createdAt description { markdown truncatedHTML } ownerId title updatedAt } } } ``` ```json { "inputs": [ { "task": { "description": { "markdown": "example _rich_ **text**" }, "title": "example value for title" } }, { "task": { "description": { "markdown": "example _rich_ **text**" }, "title": "example value for title" } } ] } ``` ##### Output  `bulkCreateTasks` returns the set of tasks that successfully completed the action. In the JS client, the fields returned for the record can be controlled with the option. In GraphQL, the response is in the , which includes a selection that completed the action successfully.The `success` property will be `false` if any of the records failed to perform their action and the returned set of `tasks` will only include the ones that completed successfully. ```graphql """ The output when running the create on the task model in bulk. """ type BulkCreateTasksResult { """ Boolean describing if all the bulk actions succeeded or not """ success: Boolean! """ Aggregated list of errors that any bulk action encountered while processing """ errors: [ExecutionError!] """ The list of all changed task records by each sent bulk action. Returned in the same order as the input bulk action params. """ tasks: [Task] } ``` ### Bulk task `update`  ##### Input  `bulkUpdateTasks` operates on a set of tasks, identified by the `ids` variable. ```typescript const taskRecords = await api.task.bulkUpdate([ { description: { markdown: "example _rich_ **text**", }, id: "1", owner: { _link: "1", }, }, { description: { markdown: "example _rich_ **text**", }, id: "2", owner: { _link: "1", }, }, ]); ``` ```tsx const RunActionComponent = () => { const [{ data, error, fetching }, bulkUpdate] = useBulkAction(api.task.bulkUpdate); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation BulkUpdateTasks($inputs: [BulkUpdateTasksInput!]!) { bulkUpdateTasks(inputs: $inputs) { success errors { message } tasks { id state createdAt description { markdown truncatedHTML } ownerId title updatedAt } } } ``` ```json { "inputs": [ { "task": { "description": { "markdown": "example _rich_ **text**" }, "owner": { "_link": "1" } }, "id": "1" }, { "task": { "description": { "markdown": "example _rich_ **text**" }, "owner": { "_link": "1" } }, "id": "2" } ] } ``` ##### Output  `bulkUpdateTasks` returns the set of tasks that successfully completed the action. In the JS client, the fields returned for the record can be controlled with the option. In GraphQL, the response is in the , which includes a selection that completed the action successfully.The `success` property will be `false` if any of the records failed to perform their action and the returned set of `tasks` will only include the ones that completed successfully. ```graphql """ The output when running the update on the task model in bulk. """ type BulkUpdateTasksResult { """ Boolean describing if all the bulk actions succeeded or not """ success: Boolean! """ Aggregated list of errors that any bulk action encountered while processing """ errors: [ExecutionError!] """ The list of all changed task records by each sent bulk action. Returned in the same order as the input bulk action params. """ tasks: [Task] } ``` ### Bulk task `delete`  `bulkDeleteTasks` action destroys the records. ##### Input  `bulkDeleteTasks` operates on a set of tasks, identified by the `ids` variable. ```typescript await api.task.bulkDelete(["1", "2"]); ``` ```tsx const RunActionComponent = () => { const [{ data, error, fetching }, bulkDelete] = useBulkAction(api.task.bulkDelete); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation BulkDeleteTasks($ids: [GadgetID!]!) { bulkDeleteTasks(ids: $ids) { success errors { message } } } ``` ```json { "ids": ["1", "2"] } ``` ##### Output  `bulkDeleteTasks` deletes the record, so it returns `void` in the JS client. In GraphQL it returns only the `success` and `errors` from the . ```graphql """ The output when running the delete on the task model in bulk. """ type BulkDeleteTasksResult { """ Boolean describing if all the bulk actions succeeded or not """ success: Boolean! """ Aggregated list of errors that any bulk action encountered while processing """ errors: [ExecutionError!] } ``` ### Bulk task `upsert`  ##### Input  `bulkUpsertTasks` operates on a set of tasks, identified by the `ids` variable. ```typescript const results = await api.task.bulkUpsert([ { id: "1", title: "example value for title", }, { id: "1", title: "example value for title", }, ]); ``` ```tsx const RunActionComponent = () => { const [{ data, error, fetching }, bulkUpsert] = useBulkAction(api.task.bulkUpsert); return ( <> Result: {JSON.stringify(data)} ); }; ``` ```graphql mutation BulkUpsertTasks($inputs: [BulkUpsertTasksInput!]!) { bulkUpsertTasks(inputs: $inputs) { success errors { message } results } } ``` ```json { "inputs": [ { "task": { "id": "1", "title": "example value for title" } }, { "task": { "id": "1", "title": "example value for title" } } ] } ``` ##### Output  `bulkUpsertTasks` returns the set of tasks that successfully completed the action. In the JS client, the fields returned for the record can be controlled with the option. In GraphQL, the response is in the , which includes a selection that completed the action successfully.The `success` property will be `false` if any of the records failed to perform their action and the returned set of `tasks` will only include the ones that completed successfully. ```graphql """ The result of a bulk upsert operation for the task model """ type BulkUpsertTasksResult { """ Boolean describing if all the bulk actions succeeded or not """ success: Boolean! """ Aggregated list of errors that any bulk action encountered while processing """ errors: [ExecutionError!] """ The results of each upsert action in the bulk operation """ tasks: [Task] } ```