Open Function StandardWriting OFS Functions

Writing OFS Functions

Project structure

my-function/
├── aerostack.json      # OFS manifest (required)
├── src/
│   └── index.ts        # Function entry point
├── tests/
│   └── index.test.ts   # Optional tests
└── package.json

Function signature

export default async function(
  inputs: Record<string, any>,  // Validated against your manifest's input schema
  context: {
    sdk: AerostackSDK,   // Injected by the runtime
    user?: User,         // Present when auth: "required" or "optional"
    env: Env,            // Runtime environment variables
    meta: {
      projectId: string,
      functionName: string,
    }
  }
): Promise<Record<string, any>>  // Validated against output schema

Example: user profile function

// aerostack.json
{
  "name": "get-user-profile",
  "version": "1.0.0",
  "description": "Get a user's profile with cached lookups",
  "runtime": "any",
  "inputs": {
    "userId": { "type": "string", "required": true }
  },
  "outputs": {
    "user": { "type": "object", "required": true },
    "fromCache": { "type": "boolean" }
  },
  "requires": ["db", "cache"],
  "auth": "required"
}
// src/index.ts
export default async function({ userId }, { sdk, user }) {
  // Only return your own profile (or admin can see any)
  if (userId !== user.id && !user.customFields?.isAdmin) {
    throw new Error('Forbidden')
  }
 
  const cacheKey = `user:${userId}`
 
  const cached = await sdk.cache.get(cacheKey)
  if (cached) return { user: cached, fromCache: true }
 
  const profile = await sdk.db.queryOne(
    'SELECT id, email, name, avatar_url, created_at FROM users WHERE id = ?',
    [userId]
  )
 
  if (!profile) throw new Error('User not found')
 
  await sdk.cache.set(cacheKey, profile, { ttl: 300 })
 
  return { user: profile, fromCache: false }
}

Input validation

OFS validates inputs against the manifest schema before your function runs. Invalid inputs throw a 400 VALIDATION_ERROR before your code executes:

"inputs": {
  "email": { "type": "string", "required": true, "format": "email" },
  "age": { "type": "number", "min": 0, "max": 150 },
  "role": { "type": "string", "enum": ["admin", "user", "viewer"] },
  "tags": { "type": "array", "items": { "type": "string" } }
}

Error handling

Throw plain errors — OFS wraps them in a standard response:

throw new Error('User not found')
// → 404 { "error": "USER_NOT_FOUND", "message": "User not found" }
 
throw Object.assign(new Error('Rate limit exceeded'), { code: 'RATE_LIMITED', status: 429 })
// → 429 { "error": "RATE_LIMITED", "message": "Rate limit exceeded" }

Testing locally

// tests/index.test.ts
import { testFunction } from '@aerostack/test-utils'
import fn from '../src/index'
 
test('returns user profile', async () => {
  const result = await testFunction(fn, {
    inputs: { userId: 'test-user-id' },
    mockSDK: {
      db: {
        queryOne: () => ({ id: 'test-user-id', email: '[email protected]' })
      },
      cache: { get: () => null, set: () => {} }
    },
    user: { id: 'test-user-id' }
  })
 
  expect(result.user.email).toBe('[email protected]')
  expect(result.fromCache).toBe(false)
})

Run locally

aerostack dev
# → Starts a local OFS runner on http://localhost:8787
# → POST http://localhost:8787/ with JSON body to invoke your function