FullStacked Cloud · Documentation docs.fullstacked.cloud
View ?format=md View ?format=json
AI agents and scripts: https://docs.fullstacked.cloud/?format=md https://docs.fullstacked.cloud/?format=json MCP:docs_get({ topic: "runtime" })

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

ts
// 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();
}
ExportRequiredRules
export const actionYes (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 defaultYes(params, ctx?) => result. Receives validated params with schema defaults applied. The result must be JSON-serializable (undefined becomes null).
export function setup(params)NoRemote runs only. Opens resources; returns ctx.
export function teardown(ctx)NoRemote 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/*.ts files 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 other actions/*.ts name 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

tsx
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.

http
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:

json
{
  "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" }
}
StatusWhen
succeededThe handler returned; output is its JSON result.
failedsetup 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.
timeoutThe run exceeded 30 s (ACTION_TIMEOUT_MS) and was killed.

Other endpoints (same host, Authorization: Bearer):

EndpointReturns
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.

LimitValue
Timeout30 s by default
Memory512 MiB, no swap
CPU / processes1 vCPU, 256 processes
NetworkInternet allowed (including tunnels.fullstacked.cloud, so managed data and Edge tunnels work through fullstacked/tunnel); private networks and the host are blocked
PackagesBundled 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:

VariableValue
AUTH_TOKENThe caller's own fsc_* token (calls to /api/v1/me return the caller).
ACTION_NAME, ACTION_RUN_ID, ACTION_PARAMSName, run id, validated params as JSON.
RELEASE_TAG, PROJECT_IDThe release and project being run.
FULLSTACKED_ACTION_RUN"1".
ACTION_TEARDOWN_TIMEOUT_MSTeardown 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:

json
{
  "tag": "v1.0.0",
  "params": {
    "customerId": "cus_123"
  }
}

curl:

bash
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_*