Build workflows with App Actions
How an App Action workflow is built in code: the definition, execute(context), triggers, where it runs, what comes back, limits, and errors.
An App Action is JavaScript that Fliplet runs for your project on demand, on a schedule, or in response to an event. Use actions to automate work so it happens without a person pressing a button.
This page is for developers. It applies to Fliplet Studio, the Fliplet JavaScript SDK, and the Fliplet API. You work with actions through Fliplet.App.V3.Actions or the REST routes under /v3/apps/:appId/actions, and you can see a project's actions under Actions in the project sidebar, which opens Action Builder.
Every route needs an authenticated request. REST requests send the token in the Auth-Token header, and the SDK adds the current user's token for you. Creating, changing, and deleting an action needs editor access to the project. Publishing and unpublishing need publisher access. The run route has no editor check. For what an action can read and write, see Authentication and permissions.
How a workflow fits together
An action has four parts:
- Code: one
execute(context)function. The steps of your workflow are the statements inside that function. - Trigger: what starts the action:
manual,schedule,log, oranalytics. - Environment: where the action runs:
server,client, orany. - Publishing: copies the action to the published software.
Example
This action emails a team when a row is added to a data source (a table of data your software stores in Fliplet). The log trigger starts it, and the code reads the new row.
const action = await Fliplet.App.V3.Actions.create({
name: 'notify-new-order',
description: 'Emails the sales team when an order is added.',
active: true,
environment: 'server',
triggers: [{
trigger: 'log',
where: { type: 'dataSource.entry.create', dataSourceId: 123 }
}],
dependencies: ['fliplet-datasources', 'fliplet-communicate'],
code: `
async function execute(context) {
try {
const log = context.payload.log;
const connection = await Fliplet.DataSources.connect(log.dataSourceId);
const entry = await connection.findById(log.dataSourceEntryId);
await Fliplet.Communicate.sendEmail({
to: [{ email: 'sales@example.com', name: 'Sales team', type: 'to' }],
subject: 'New order',
html: 'New order from ' + entry.data.Customer
});
return { success: true };
} catch (err) {
return { success: false, error: err.message };
}
}`
});
This screen code runs a server action by name, waits for it, and checks the outcome:
const outcome = await Fliplet.App.V3.Actions.runWithResult('calculate-total', { orderId: 42 });
if (!outcome || outcome.success === false) {
console.error('The action failed', outcome);
} else {
console.log(outcome.data);
}
Action definition
POST /v3/apps/:appId/actions creates an action, and Fliplet.App.V3.Actions.create(data) does the same from code. PUT /v3/apps/:appId/actions/:id changes one and merges your fields into the stored action. The saved action comes back with isPublished.
| Field | Type | Required | Default | Accepts |
|---|---|---|---|---|
name | String | Yes | None | 255 characters at most: letters, numbers, dashes, and underscores. Unique in the app. |
code | String | Yes | None | JavaScript that defines an async execute function. |
description | String | No | None | 1,000 characters at most. |
active | Boolean | No | false | An inactive action doesn't run on a schedule or from a trigger, and a run request is refused. |
environment | String | No | server | server, client, or any. |
triggers | Array | No | Empty | Objects with trigger and an optional where. |
frequency | String | No | None | A 5-part cron expression: minute hour day month weekday. * * * * * is refused. |
timezone | String | No | None | A timezone name such as Europe/London, used to work out the schedule. |
dependencies | Array | No | Empty | Fliplet package names, or https URLs of scripts. |
Code that uses a Fliplet API from a package, such as Fliplet.DataSources, Fliplet.Communicate, or Fliplet.Notifications, fails validation unless that package is in dependencies. For everything else the validator checks, see Code validator rules for App Actions.
execute(context)
Write the code as a single async function execute(context).
| Property | Contains |
|---|---|
context.payload | The object passed to the run, or {} when there's none. |
context.settings | The project's private settings (top-level keys starting with _) in a server action. It's {} everywhere else. |
A value saved from a secure panel field with settingKey: 'foo' is read as settings._aiartifact_foo. For more on inputs, see Inputs, outputs and JSON contracts.
To mark a server run as failed, return { success: false, error }. A run can be repeated or overlap another run, so running it twice must leave the same data.
On the server, your code runs in a page with fliplet-runtime and fliplet-core loaded, plus your dependencies. When the definition sets environment: 'server', code that mentions document., window., $(, or jQuery( fails validation.
To call another service from a server action, use Fliplet.App.V3.Actions.request({ url, method, headers, body, timeoutMs }). The URL must be https. The request resolves with { status, headers, body }, and body is text. timeoutMs defaults to 30,000 and is capped at 50,000. In client and any actions, request() fails with the code UNAVAILABLE.
Triggers
| Trigger | Starts when | Environments | Payload |
|---|---|---|---|
manual | Your code asks for a run | server, client, any | What the caller passes |
schedule | The frequency time arrives | server, any | Empty |
log | Fliplet saves a log row that matches | server, any | { trigger: 'log', log } |
analytics | The software records an analytics event on the device | client, any | The event: { createdAt, type, data } |
A run request doesn't check the triggers list, so code can start any active action. The exception is that REST can't run a client action.
The frequency value drives a schedule: an active action with a frequency is queued when it's saved.
For a log trigger, log is the row, including type, dataSourceId, and dataSourceEntryId. Data source changes log as dataSource.entry.create, dataSource.entry.update, and dataSource.entry.delete. Only active actions in the app attached to the write are considered. If the write has no app, none run.
Analytics events reach the trigger with the type analytics.event or analytics.pageView. Action code running on the server generates no analytics events.
Use where to filter a trigger. It's an object, and only the keys you list are compared. For log, it's compared to the log row. For analytics, use { type, data }. A where that isn't an object is ignored, so the trigger matches every event of that type. The AI chat in Fliplet Studio sends where as JSON text and parses it first, so the object form is what's stored.
> [!WARNING] > An action with a log trigger that writes a matching row can start itself again. The trigger code has no check for who made the write.
Environments
| Situation | server | client | any |
|---|---|---|---|
| Started by screen code | Runs on the server | Runs in the browser | Runs in the browser |
| Started by REST | Runs on the server | Refused | Runs on the server |
Started by a schedule or log trigger | Runs on the server | Not allowed | Runs on the server |
Started by an analytics trigger | Not allowed | Runs in the browser | Runs in the browser |
context.settings | Private settings | {} | {} |
request() | Available | UNAVAILABLE | UNAVAILABLE |
Screen code finds client and any actions in a bundle it loads for the app. An action that isn't in the bundle goes to the server.
A server run has 870 seconds and a browser run has 30 seconds. The browser limit doesn't stop your code: the error is raised when the code finishes after 30 seconds.
> [!CAUTION] > Security: The code of client and any actions is sent to the device. Keep credentials in server actions.
Run an action from code
| Call | What it does |
|---|---|
Fliplet.App.V3.Actions.run(id, payload) | Starts the action and doesn't return its result. |
Fliplet.App.V3.Actions.runWithResult(id, payload) | Waits for the result. |
POST /v3/apps/:appId/actions/:id/run | The body is the payload. Add ?withResult to wait. |
id is the action's ID or name.
What comes back
| Run | runWithResult gives you |
|---|---|
| Server, completed | Resolves with { success: true, data }, where data is what execute returned. |
| Server, failed | Resolves with { success: false, error }. error.message holds the message. |
| Server, runner couldn't finish, such as a timeout | Resolves with undefined. |
| Browser, completed | Resolves with the value execute returned, unwrapped. |
Browser, returned { success: false } | Not treated as a failure: the object comes back as is. |
| Browser, threw an error | The promise rejects. |
| Browser, inactive action | Logs to the console and resolves with undefined. |
A failed server run doesn't reject the promise, so always check the outcome. On the server path, run resolves with undefined.
REST with ?withResult responds with HTTP 200 and status: ACTION_EXECUTION_COMPLETED even when the action failed. The body has result, duration, userId, and on failure error. Without ?withResult, status is ACTION_EXECUTION_STARTED.
Each server run saves a log of type app.task.completed or app.task.failed. Read them with Fliplet.App.V3.Actions.getLogs(), and see Test and debug an App Action.
Make an action live
The project and the published software each hold their own copy of an action. Saving changes only the project's copy. Live software keeps the last published copy until you publish again.
- The software must be published first.
- Publishing an already published action updates that copy in place, with the same ID and run logs.
- Unpublishing deletes the published copy. Publishing again creates one with a new ID.
- Live software can't run an action that isn't published. The request fails with HTTP 404.
- The published copy has its own ID, so run actions by name.
- A
logtrigger runs the copy in the app attached to the write. [UNVERIFIED: which copy runs for a write from live software]
> [!WARNING] > An active action with a frequency is queued as soon as it's saved, before you publish. Publishing queues the published copy on its own schedule and doesn't remove the project copy's job, so both run.
Limits
| Limit | Value |
|---|---|
| Server run | 870 seconds |
| Browser run | 30 seconds |
| Payload on a server run | Travels in a URL that can't exceed 16,384 characters |
| Private settings on a server run | 100 KB, or the run fails |
| Run requests | 31 per 60 seconds, per user and IP address |
| Create, read, update, and delete requests | 61 per 60 seconds, per user and IP address |
| Publish and unpublish requests | 11 per 60 seconds, per user and IP address |
| Bundle requests | 121 per 60 seconds, per user and IP address |
request() body and response | 5 MB |
request() redirects | 5 |
Over a rate limit, the response is HTTP 429 with message, referenceUrl, and nextValidRequestDate.
Queued runs, from a schedule or a log trigger, also count against per-minute and per-month plan limits. See Plans and usage limits.
When an organization is out of credits, a queued run is skipped without a charge and the schedule carries on. A run request on software that uses credits fails with HTTP 402 and error: 'insufficient_credits' when the organization doesn't have the credits. Enterprise and internal organizations aren't blocked. See Understand Fliplet credits.
Errors
These are the common errors, by status value. For the full list, see Errors and failure behavior.
| Status | HTTP | Cause |
|---|---|---|
NAME_ALREADY_EXISTS | 409 | Another action in the app has this name. |
CODE_VALIDATION_FAILED | 400 | A syntax error, a blocked pattern, no execute function, or a missing dependency. |
TRIGGER_NOT_ALLOWED | 400 | The trigger isn't allowed in this environment. |
FREQUENCY_INVALID | 400 | frequency isn't a valid cron expression. |
FREQUENCY_TOO_FREQUENT | 400 | frequency is * * * * *. |
TIMEZONE_INVALID | 400 | timezone isn't a valid timezone name. |
DEPENDENCIES_INVALID | 400 | A dependency couldn't be resolved. |
ACTION_NOT_FOUND | 404 | The action exists in the other copy only, for example an unpublished action run from live software. |
ACTION_INACTIVE | 400 | The action isn't active. |
CLIENT_ACTION_NOT_RUNNABLE | 400 | REST can't run a client action. |
APP_NOT_PUBLISHED | 400 | Publish the software before publishing the action. |
CANNOT_UPDATE_PRODUCTION, CANNOT_DELETE_PRODUCTION | 403 | The published copy can't be changed or deleted directly. |
EXECUTION_FAILED | 500 | The run request itself failed. |
A name or ID that doesn't exist in the project or the published software gets HTTP 404 with the message App Task not found.
See also
- How to use Actions: create an action with the AI chat in Fliplet Studio and make it live.
- Create and configure an App Action: the steps to set one up.
- Test and debug an App Action: test runs and logs.
- App Action lifecycle and runtime differences: more on publishing and on server and browser behavior.
- App Actions reference: every method and route.