Project Actions
An action is a TypeScript file actions/<name>.ts in a project. Its default export is an ordinary async function: the app imports and calls it directly, and once a v* release tag is built, any authorized caller can run the same function remotely in a sandbox with POST https://git.fullstacked.cloud/api/v1/projects/:id/actions/:name/run (MCP: projects_run_action). Remote runs get the caller's environment and token, so data access follows the caller's permissions.
File contract
// actions/add-todo.ts
import type pg from "pg";
import { getPool } from "../db";
export const action = {
title: "Add Todo",
description: "Creates a todo item",
parameters: {
type: "object",
properties: {
title: { type: "string", minLength: 1, description: "Todo title" },
priority: { type: "string", enum: ["low", "medium", "high"], default: "medium" },
},
required: ["title"],
additionalProperties: false,
},
};
type Params = { title: string; priority?: "low" | "medium" | "high" };
/** Remote runs only: runs first, its return value is the handler's ctx. */
export async function setup(params: Params) {
return { pool: await getPool() };
}
/** The handler: called by the app directly (no ctx) and by remote runs. */
export default async function addTodo(params: Params, ctx?: { pool: pg.Pool }) {
const pool = ctx?.pool ?? (await getPool());
const { rows } = await pool.query(
"INSERT INTO todos (title, priority) VALUES ($1, $2) RETURNING *",
[params.title, params.priority ?? "medium"]
);
return rows[0];
}
/** Remote runs only: always runs after the handler, even when it threw. */
export async function teardown(ctx?: { pool: pg.Pool }) {
await ctx?.pool.end();
}| Export | Required | Rules |
|---|---|---|
export const action | Yes (the release build fails without it) | A static literal: objects, arrays, strings (template strings without ${}), numbers, booleans, null, comments, trailing commas; a type annotation, as const and satisfies are allowed. No identifiers, calls, spreads or shorthand properties. Fields: title? (defaults to the name), description?, parameters? (a JSON Schema whose root is { "type": "object" }, default no properties), name? (must equal the file name). |
export default | Yes | (params, ctx?) => result. Receives validated params with schema defaults applied. The result must be JSON-serializable (undefined becomes null). |
export function setup(params) | No | Remote runs only. Opens resources; returns ctx. |
export function teardown(ctx) | No | Remote runs only. Always runs, bounded by 5 s (ACTION_TEARDOWN_TIMEOUT_MS); its errors are logged and never change the outcome. |
File rules:
- Only top-level
actions/*.tsfiles are actions. Files starting with_or.,*.d.ts, subdirectories and other extensions are helpers (actions/_shared.ts). - The file name is the action name:
^[a-z0-9][a-z0-9_-]{0,63}$. Any otheractions/*.tsname fails the build. - Never run the handler at module top level: remote runs import the module, so top-level side effects run too. Detect a remote run with
process.env.FULLSTACKED_ACTION_RUN === "1"if needed. - Hooks are plain exports and can be shared:
export { setup, teardown } from "./_lifecycle";.
Parameter schema
Supported keywords: type (string, number, integer, boolean, object, array, null, or a list), properties, required, additionalProperties, items, enum, const, default, minLength, maxLength, pattern, minimum, maximum, exclusiveMinimum, exclusiveMaximum, minItems, maxItems. Other keywords are ignored. Invalid params answer 400 INVALID_PARAMS naming the parameter, before anything runs. required is an array on the object schema, not a flag on each property.
Calling from the app
import addTodo from "./actions/add-todo";
async function onSubmit(title: string) {
const todo = await addTodo({ title });
}In the app no schema validation or hooks run: the handler gets exactly what you pass, and resources live as long as the app.
Remote runs
Actions run only from a successfully built release: push a tag starting with v (MCP projects_create_tag), the build bundles each action with its dependencies, and an invalid action fails the build.
POST https://git.fullstacked.cloud/api/v1/projects/<projectId>/actions/add-todo/run
Authorization: Bearer <fsc_* token>
Content-Type: application/json
{ "tag": "v1.2.0", "params": { "title": "Ship docs" } }tag is optional: the newest succeeded release the caller can access is used. The response is 200 even when the run failed or timed out:
{
"run_id": "3c1f0e2a-...",
"action": "add-todo",
"tag": "v1.2.0",
"status": "succeeded",
"exit_code": 0,
"error": null,
"duration_ms": 1840,
"created_at": "2026-10-23T14:02:11.000Z",
"output": { "id": 42, "title": "Ship docs", "priority": "medium" }
}| Status | When |
|---|---|
succeeded | The handler returned; output is its JSON result. |
failed | setup or the handler threw (error is its message; an error with a 5-character code such as a PostgreSQL SQLSTATE becomes SQLSTATE <code>: <message>), a non-zero exit, or no valid result. |
timeout | The run exceeded 30 s (ACTION_TIMEOUT_MS) and was killed. |
Other endpoints (same host, Authorization: Bearer):
| Endpoint | Returns |
|---|---|
GET /api/v1/projects/:id/actions?tag= | { tag, actions: [{ name, title, description, file, bundle, parameters }] } of a release (MCP projects_list_actions). |
GET /api/v1/projects/:id/actions/runs?tag=&action=&status=&limit= | { runs: [{ run_id, action, tag, caller, caller_name, status, exit_code, error, duration_ms, created_at }] }, newest first (limit 1 to 200, default 50). Users without read access see only their own runs. |
GET /api/v1/projects/:id/actions/runs/:runId | { run }: the same fields plus output, logs (last 256 KiB of stdout and stderr) and finished_at. |
Errors: 400 INVALID_PARAMS params rejected by the schema, 403 tag not accessible, 404 unknown action, tag or release without a successful build, 502 environment resolution failed, 503 sandbox unavailable.
Sandbox environment
Each run is a fresh container running the bundled action with fullstacked --file (runtime 1.0.0-alpha.1844). The code runs under Node: process is Node's own, there is no document, fullstacked/auth (which opens a window) cannot be used, and a relative fetch("/...") fails. Keep UI code out of the modules an action imports.
| Limit | Value |
|---|---|
| Timeout | 30 s by default |
| Memory | 512 MiB, no swap |
| CPU / processes | 1 vCPU, 256 processes |
| Network | Internet allowed (including tunnels.fullstacked.cloud, so managed data and Edge tunnels work through fullstacked/tunnel); private networks and the host are blocked |
| Packages | Bundled at release time; nothing is installed at run time |
process.env holds the project environment resolved for the caller (Injected process.env), so a read-only caller gets read-only database credentials and writes fail. On top of it:
| Variable | Value |
|---|---|
AUTH_TOKEN | The caller's own fsc_* token (calls to /api/v1/me return the caller). |
ACTION_NAME, ACTION_RUN_ID, ACTION_PARAMS | Name, run id, validated params as JSON. |
RELEASE_TAG, PROJECT_ID | The release and project being run. |
FULLSTACKED_ACTION_RUN | "1". |
ACTION_TEARDOWN_TIMEOUT_MS | Teardown bound in ms. |
These run variables override custom project variables of the same name. Output written to console.log / console.error is kept as logs; only the handler's return value is the result. Open pools or sockets cannot keep a run alive: the runner exits after teardown.
Who can run
Anyone with access to the release tag (owner, any project tier, or a release-only grant) can list and run actions; the run executes with that caller's permissions on linked data and tunnels.
Endpoints
GET /api/v1/projects/:id/actions
Actions compiled in a release (?tag=, default the newest release the caller can access).
- Service:
git(https://git.fullstacked.cloud) - Auth: Bearer fsc_*
POST /api/v1/projects/:id/actions/:name/run
Runs a release's action with the caller's own token and data wiring; answers 200 with the run result even when the run failed or timed out.
- Service:
git(https://git.fullstacked.cloud) - Auth: Bearer fsc_*
Request:
{
"tag": "v1.0.0",
"params": {
"customerId": "cus_123"
}
}curl:
curl -sS -X POST -H "Authorization: Bearer $AUTH_TOKEN" -H "Content-Type: application/json" -d '{"tag":"v1.0.0","params":{"customerId":"cus_123"}}' "https://git.fullstacked.cloud/api/v1/projects/<projectId>/actions/sync-orders/run"GET /api/v1/projects/:id/actions/runs
Past action runs, filtered by ?tag=, ?action=, ?status= and ?limit=.
- Service:
git(https://git.fullstacked.cloud) - Auth: Bearer fsc_*
GET /api/v1/projects/:id/actions/runs/:runId
One action run.
- Service:
git(https://git.fullstacked.cloud) - Auth: Bearer fsc_*