Skill: juliendsv/poststash (version 0.1.0)

--- name: poststash description: Schedule posts and threads to Threads and X via PostStash version: 1.0.0 author: Julien / PostStash homepage: https://poststash.com/docs/api license: MIT metadata: { "openclaw": { "emoji": "📮", "requires": { "env": ["POSTSTASH_API_KEY"] } } } --- # PostStash Skill Schedule posts and threads to Threads and X directly from your OpenClaw agent. ## Setup 1. Get an API key from the [PostStash dashboard](https://poststash.com/dashboard/api-keys) 2. Set the environment variable: ```bash POSTSTASH_API_KEY=ps_live_... ``` 3. Install: `clawhub install poststash` API key format: `ps_live_` followed by 64 hex characters. --- ## Tools ### `poststash.schedule_to_threads` Schedule a single post to Threads. **Input:** - `text` (string, required) — Post content (max 500 characters) - `scheduled_at` (string, optional) — ISO 8601 timestamp; defaults to now - `status` (string, optional) — `"Ready"` (publishes at scheduled_at) or `"Draft"` (saves without scheduling); defaults to `"Ready"` **Output:** - `post_id` — UUID of the scheduled post - `content` — Post text - `schedule` — Scheduled timestamp (ISO 8601) - `status` — Post status (`Ready`, `Draft`, `Published`, `Failed`) - `platforms` — Array of target platforms --- ### `poststash.schedule_to_x` Schedule a single post to X (Twitter). **Input:** Same as `schedule_to_threads`. **Output:** Same as `schedule_to_threads`. --- ### `poststash.schedule_thread` Schedule a thread (2–20 posts) to Threads or X. **Input:** - `platform` (string, required) — `"threads"` or `"x"` - `posts` (array, required) — Array of `{ text: string }` objects (2–20 items) - `scheduled_at` (string, optional) — ISO 8601 timestamp; defaults to now - `status` (string, optional) — `"Ready"` or `"Draft"`; defaults to `"Ready"` **Output:** - `thread_id` — UUID grouping all thread posts - `posts` — Array of scheduled post objects with `id`, `content`, `thread_position` --- ### `poststash.get_post` Fetch a scheduled or published post along with its analytics. **Input:** - `post_id` (string, required) — Post UUID from a schedule response **Output:** - `post` — Full post object (`id`, `content`, `platforms`, `schedule`, `status`, `published`, `sent_at`) - `analytics` — Array of per-platform analytics with `metrics` (views, likes, replies, reposts, quotes) --- ### `poststash.list_posts` List posts for your account (scoped to the context of your API key). **Input:** - `status` (string, optional) — Filter by `"Draft"`, `"Ready"`, `"Published"`, or `"Failed"` - `published` (boolean, optional) — `true` or `false` - `limit` (number, optional) — Results per page (default: 20, max: 100) - `offset` (number, optional) — Pagination offset (default: 0) **Output:** - `posts` — Array of post objects - `total` — Total number of matching posts - `limit` / `offset` — Pagination info --- ## Example Agent Usage ```typescript const result = await agent.invoke({ tools: ["poststash.schedule_to_threads"], input: "Schedule a post about AI to Threads tomorrow at 2pm", }); const thread = await agent.invoke({ tools: ["poststash.schedule_thread"], input: "Create a 3-part thread on building in public on X, schedule for Friday 10am", }); ```

path: README.md
content: # @openclaw-skill/poststash

Schedule posts and threads to [Threads](https://threads.net) and [X (Twitter)](https://x.com) from OpenClaw agents via [PostStash](https://poststash.com).

## Installation

```bash
clawhub install poststash
```

## Setup

1. Go to your [PostStash dashboard](https://poststash.com/dashboard/api-keys) and generate an API key
2. Set the environment variable:
   ```bash
   export POSTSTASH_API_KEY=ps_live_...
   ```

API key format: `ps_live_` followed by 64 hex characters.

---

## Tools

### `poststash.schedule_to_threads`

Schedule a single post to Threads.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `text` | string | Yes | Post content (max 500 chars) |
| `scheduled_at` | string | No | ISO 8601 timestamp; defaults to now |
| `status` | `"Ready"` \| `"Draft"` | No | Defaults to `"Ready"` |

**Response:** `{ post_id, content, schedule, status, platforms }`

---

### `poststash.schedule_to_x`

Schedule a single post to X (Twitter).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `text` | string | Yes | Post content (max 280 chars) |
| `scheduled_at` | string | No | ISO 8601 timestamp; defaults to now |
| `status` | `"Ready"` \| `"Draft"` | No | Defaults to `"Ready"` |

**Response:** `{ post_id, content, schedule, status, platforms }`

---

### `poststash.schedule_thread`

Schedule a thread (2–20 posts) to Threads or X.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `platform` | `"threads"` \| `"x"` | Yes | Target platform |
| `posts` | `{ text: string }[]` | Yes | 2–20 posts in the thread |
| `scheduled_at` | string | No | ISO 8601 timestamp; defaults to now |
| `status` | `"Ready"` \| `"Draft"` | No | Defaults to `"Ready"` |

**Response:** `{ thread_id, posts: [{ id, content, thread_position, schedule, status }] }`

---

### `poststash.get_post`

Fetch a post and its analytics.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `post_id` | string | Yes | Post UUID |

**Response:** `{ post, analytics }`

The `analytics` array contains per-platform metrics: `views`, `likes`, `replies`, `reposts`, `quotes`.

---

### `poststash.list_posts`

List posts scoped to your API key's context.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | `"Draft"` \| `"Ready"` \| `"Published"` \| `"Failed"` | No | Filter by status |
| `published` | boolean | No | Filter by published state |
| `limit` | number | No | Results per page (default: 20, max: 100) |
| `offset` | number | No | Pagination offset (default: 0) |

**Response:** `{ posts, total, limit, offset }`

---

## Examples

### Schedule a single post

```typescript
import { scheduleToThreads } from "@openclaw-skill/poststash";

const result = await scheduleToThreads.execute({
  text: "AI agents can now schedule posts via PostStash 🤖",
  scheduled_at: "2025-03-01T14:30:00Z",
});
console.log(result.post_id); // UUID of scheduled post
```

### Schedule a thread

```typescript
import { scheduleThread } from "@openclaw-skill/poststash";

const result = await scheduleThread.execute({
  platform: "x",
  posts: [
    { text: "1/ Here is a thread about building in public" },
    { text: "2/ Ship fast, learn faster" },
    { text: "3/ That's a wrap. Follow for more 👋" },
  ],
  scheduled_at: "2025-03-01T14:30:00Z",
});
console.log(result.thread_id);
```

### Check post status

```typescript
import { getPost } from "@openclaw-skill/poststash";

const { post, analytics } = await getPost.execute({
  post_id: "6b79ac1e-fc70-4a5e-a45f-4769f47300b5",
});
console.log(post.status);     // "Published"
console.log(analytics[0].metrics.views); // 2340
```

### List recent posts

```typescript
import { listPosts } from "@openclaw-skill/poststash";

const { posts, total } = await listPosts.execute({
  status: "Ready",
  limit: 10,
});
console.log(`${total} ready posts, showing first ${posts.length}`);
```

### Agent usage

```typescript
const result = await agent.invoke({
  tools: ["poststash.schedule_to_threads"],
  input: "Schedule a post about AI to Threads tomorrow at 2pm",
});
```

---

## Error Handling

All tools throw an `Error` with the API error message on failure. Common errors:

| Error | Fix |
|-------|-----|
| `POSTSTASH_API_KEY environment variable is not set` | Set the env var |
| `Unauthorized` | Check your API key format (`ps_live_...`) |
| `platform must be one of: threads, x` | Use `"threads"` or `"x"` |
| `text is required` | Add text to the post |
| `posts must have at least 2 posts` | Add more posts to the thread |
| `A thread can have at most 20 posts` | Reduce thread length |

---

## Development

```bash
npm run build   # Compile TypeScript
npm run test    # Run tests
```

---

## Links

- [PostStash](https://poststash.com)
- [API Documentation](https://poststash.com/docs/api)
- [Dashboard / API Keys](https://poststash.com/dashboard/api-keys)
- [OpenClaw](https://openclaw.ai)


path: package.json
content: {
  "name": "@openclaw-skill/poststash",
  "version": "1.0.0",
  "description": "Schedule posts and threads to Threads and X via PostStash",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
  "openclaw": {
    "type": "skill",
    "tools": [
      "poststash.schedule_to_threads",
      "poststash.schedule_to_x",
      "poststash.schedule_thread",
      "poststash.get_post",
      "poststash.list_posts"
    ]
  },
  "keywords": ["openclaw", "poststash", "threads", "twitter", "x", "social-media", "scheduling"],
  "license": "MIT"
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "examples"]
}


path: src/client.ts
content: const BASE = "https://poststash.com/api";

export async function poststashFetch(path: string, init: RequestInit = {}): Promise<unknown> {
  const [REDACTED_SECRET];
  if (!apiKey) {
    throw new Error("POSTSTASH_API_KEY environment variable is not set");
  }

  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  const json = await res.json();
  if (!res.ok) {
    throw new Error((json as { error?: string }).error ?? `HTTP ${res.status}`);
  }
  return json;
}


path: src/index.ts
content: export { scheduleToThreads, scheduleToX } from "./tools/schedule-post";
export { scheduleThread } from "./tools/schedule-thread";
export { getPost } from "./tools/get-post";
export { listPosts } from "./tools/list-posts";
export { poststashFetch } from "./client";


path: src/tools/get-post.ts
content: import { poststashFetch } from "../client";

interface GetPostInput {
  post_id: string;
}

interface PostMetrics {
  views: number;
  likes: number;
  replies: number;
  reposts: number;
  quotes: number;
}

interface PostAnalytics {
  platform: string;
  metrics: PostMetrics;
  last_fetched_at: string;
}

interface PostObject {
  id: string;
  content: string;
  platforms: string[];
  schedule: string | null;
  published: boolean;
  status: string;
  sent_at: string | null;
  context_id: string | null;
  created_by_source: string;
  created_by_api_key_name: string | null;
  created_at: string;
  updated_at: string;
}

interface GetPostResult {
  post: PostObject;
  analytics: PostAnalytics[];
}

export const getPost = {
  name: "poststash.get_post",
  description: "Fetch a scheduled or published post along with its analytics",
  input_schema: {
    type: "object" as const,
    properties: {
      post_id: {
        type: "string",
        description: "Post UUID from a schedule response",
      },
    },
    required: ["post_id"],
  },
  execute: async (input: GetPostInput): Promise<GetPostResult> => {
    const data = await poststashFetch(`/posts/${input.post_id}`) as {
      post: PostObject;
      analytics: PostAnalytics[];
    };

    return {
      post: data.post,
      analytics: data.analytics,
    };
  },
};


path: src/tools/schedule-thread.ts
content: import { poststashFetch } from "../client";

interface ThreadPost {
  text: string;
}

interface ScheduleThreadInput {
  platform: "threads" | "x";
  posts: ThreadPost[];
  scheduled_at?: string;
  status?: "Ready" | "Draft";
}

interface ScheduledThreadPost {
  id: string;
  content: string;
  thread_position: number;
  schedule: string | null;
  status: string;
}

interface ScheduleThreadResult {
  thread_id: string;
  posts: ScheduledThreadPost[];
}

export const scheduleThread = {
  name: "poststash.schedule_thread",
  description: "Schedule a thread (2–20 posts) to Threads or X via PostStash",
  input_schema: {
    type: "object" as const,
    properties: {
      platform: {
        type: "string",
        enum: ["threads", "x"],
        description: "Target platform",
      },
      posts: {
        type: "array",
        description: "Array of posts in the thread (2–20 items)",
        items: {
          type: "object",
          properties: {
            text: {
              type: "string",
              description: "Text content of this post in the thread",
            },
          },
          required: ["text"],
        },
        minItems: 2,
        maxItems: 20,
      },
      scheduled_at: {
        type: "string",
        description: "ISO 8601 timestamp to publish (optional, defaults to now). Example: '2025-03-01T14:30:00Z'",
      },
      status: {
        type: "string",
        enum: ["Ready", "Draft"],
        description: "Ready = publishes at scheduled_at, Draft = saves without scheduling. Defaults to Ready.",
      },
    },
    required: ["platform", "posts"],
  },
  execute: async (input: ScheduleThreadInput): Promise<ScheduleThreadResult> => {
    const body: Record<string, unknown> = {
      platforms: [input.platform],
      posts: input.posts,
    };
    if (input.scheduled_at) body.schedule = input.scheduled_at;
    if (input.status) body.status = input.status;

    const data = await poststashFetch("/posts", {
      method: "POST",
      body: JSON.stringify(body),
    }) as { thread_id: string; posts: ScheduledThreadPost[] };

    return {
      thread_id: data.thread_id,
      posts: data.posts.map((p) => ({
        id: p.id,
        content: p.content,
        thread_position: p.thread_position,
        schedule: p.schedule,
        status: p.status,
      })),
    };
  },
};


path: src/tools/list-posts.ts
content: import { poststashFetch } from "../client";

interface ListPostsInput {
  status?: "Draft" | "Ready" | "Published" | "Failed";
  published?: boolean;
  limit?: number;
  offset?: number;
}

interface PostSummary {
  id: string;
  content: string;
  platforms: string[];
  schedule: string | null;
  published: boolean;
  status: string;
  created_by_source: string;
  created_by_api_key_name: string | null;
  created_at: string;
  updated_at: string;
}

interface ListPostsResult {
  posts: PostSummary[];
  total: number;
  limit: number;
  offset: number;
}

export const listPosts = {
  name: "poststash.list_posts",
  description: "List posts for your account (scoped to the context of your API key)",
  input_schema: {
    type: "object" as const,
    properties: {
      status: {
        type: "string",
        enum: ["Draft", "Ready", "Published", "Failed"],
        description: "Filter by post status (optional)",
      },
      published: {
        type: "boolean",
        description: "Filter by published state: true (already posted) or false (pending)",
      },
      limit: {
        type: "number",
        description: "Results per page (default: 20, max: 100)",
      },
      offset: {
        type: "number",
        description: "Pagination offset (default: 0)",
      },
    },
  },
  execute: async (input: ListPostsInput = {}): Promise<ListPostsResult> => {
    const params = new URLSearchParams();
    if (input.status) params.set("status", input.status);
    if (input.published !== undefined) params.set("published", String(input.published));
    params.set("limit", String(input.limit ?? 20));
    params.set("offset", String(input.offset ?? 0));

    const query = params.toString();
    const data = await poststashFetch(`/posts?${query}`) as {
      posts: PostSummary[];
      pagination: { total: number; limit: number; offset: number };
    };

    return {
      posts: data.posts,
      total: data.pagination.total,
      limit: data.pagination.limit,
      offset: data.pagination.offset,
    };
  },
};


path: src/tools/schedule-post.ts
content: import { poststashFetch } from "../client";

interface SchedulePostInput {
  text: string;
  scheduled_at?: string;
  status?: "Ready" | "Draft";
}

interface PostResponse {
  id: string;
  content: string;
  platforms: string[];
  schedule: string | null;
  status: string;
  published: boolean;
  sent_at: string | null;
}

interface SchedulePostResult {
  post_id: string;
  content: string;
  schedule: string | null;
  status: string;
  platforms: string[];
}

async function schedulePost(
  platform: "threads" | "x",
  input: SchedulePostInput
): Promise<SchedulePostResult> {
  const body: Record<string, unknown> = {
    platforms: [platform],
    text: input.text,
  };
  if (input.scheduled_at) body.schedule = input.scheduled_at;
  if (input.status) body.status = input.status;

  const data = await poststashFetch("/posts", {
    method: "POST",
    body: JSON.stringify(body),
  }) as { post: PostResponse };

  return {
    post_id: data.post.id,
    content: data.post.content,
    schedule: data.post.schedule,
    status: data.post.status,
    platforms: data.post.platforms,
  };
}

export const scheduleToThreads = {
  name: "poststash.schedule_to_threads",
  description: "Schedule a single post to Threads via PostStash",
  input_schema: {
    type: "object" as const,
    properties: {
      text: {
        type: "string",
        description: "Post content (max 500 characters)",
      },
      scheduled_at: {
        type: "string",
        description: "ISO 8601 timestamp to publish (optional, defaults to now). Example: '2025-03-01T14:30:00Z'",
      },
      status: {
        type: "string",
        enum: ["Ready", "Draft"],
        description: "Ready = publishes at scheduled_at, Draft = saves without scheduling. Defaults to Ready.",
      },
    },
    required: ["text"],
  },
  execute: (input: SchedulePostInput) => schedulePost("threads", input),
};

export const scheduleToX = {
  name: "poststash.schedule_to_x",
  description: "Schedule a single post to X (Twitter) via PostStash",
  input_schema: {
    type: "object" as const,
    properties: {
      text: {
        type: "string",
        description: "Post content (max 280 characters)",
      },
      scheduled_at: {
        type: "string",
        description: "ISO 8601 timestamp to publish (optional, defaults to now). Example: '2025-03-01T14:30:00Z'",
      },
      status: {
        type: "string",
        enum: ["Ready", "Draft"],
        description: "Ready = publishes at scheduled_at, Draft = saves without scheduling. Defaults to Ready.",
      },
    },
    required: ["text"],
  },
  execute: (input: SchedulePostInput) => schedulePost("x", input),
};


path: examples/list-posts.ts
content: /**
 * Example: List and filter posts via PostStash
 *
 * Run with:
 *   POSTSTASH_API_KEY=ps_live_... npx ts-node examples/list-posts.ts
 */

import { listPosts } from "../src/tools/list-posts";
import { getPost } from "../src/tools/get-post";

async function main() {
  // List the 10 most recent Ready posts
  const ready = await listPosts.execute({ status: "Ready", limit: 10 });
  console.log(`Ready posts (${ready.total} total):`);
  for (const post of ready.posts) {
    console.log(`  [${post.id}] ${post.content.slice(0, 60)}...`);
    console.log(`    Platforms: ${post.platforms.join(", ")} | Schedule: ${post.schedule}`);
  }

  // List already-published posts
  const published = await listPosts.execute({ published: true, limit: 5 });
  console.log(`\nPublished posts (${published.total} total):`);
  for (const post of published.posts) {
    console.log(`  [${post.id}] ${post.content.slice(0, 60)}...`);
  }

  // Fetch a specific post with analytics (use the first published post ID if available)
  if (published.posts.length > 0) {
    const postId = published.posts[0].id;
    const detail = await getPost.execute({ post_id: postId });
    console.log("\nPost detail:", detail.post);
    if (detail.analytics.length > 0) {
      console.log("Analytics:", detail.analytics);
    }
  }
}

main().catch(console.error);


path: examples/schedule-post.ts
content: /**
 * Example: Schedule a single post to Threads and X via PostStash
 *
 * Run with:
 *   POSTSTASH_API_KEY=ps_live_... npx ts-node examples/schedule-post.ts
 */

import { scheduleToThreads, scheduleToX } from "../src/tools/schedule-post";

async function main() {
  // Schedule a post to Threads now
  const threadsResult = await scheduleToThreads.execute({
    text: "AI agents can now schedule posts via PostStash 🤖",
  });
  console.log("Scheduled to Threads:", threadsResult);

  // Schedule a post to X for 1 hour from now
  const inOneHour = new Date(Date.now() + 3_600_000).toISOString();
  const xResult = await scheduleToX.execute({
    text: "AI agents can now schedule posts via PostStash 🤖",
    scheduled_at: inOneHour,
  });
  console.log("Scheduled to X:", xResult);

  // Save as draft (won't publish automatically)
  const draftResult = await scheduleToThreads.execute({
    text: "This is a draft post — not yet scheduled",
    status: "Draft",
  });
  console.log("Saved draft:", draftResult);
}

main().catch(console.error);


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)