Where a stream is for a live trickle of events, a **channel** is for **moving a large amount of data at once**: a direct streaming pipe between two endpoints, rather than one request and response. Channels are bidirectional (each end has both a reader and a writer), but here you'll stream in one direction, uploading a CSV of links. You'll give this its own `bulk-importer` worker so the `link` worker stays focused on single links.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add iii-hq/iii --skill linkly --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Linkly?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/iii-hq-linkly-af364e26)More formats (shields.io, HTML) on the badges page.
<!-- generated by iii-skill-render. DO NOT EDIT (changes here are overwritten on the next render). Edit docs/next/tutorials/linkly/channels.mdx. -->
# Ch. 6: Move bulk data with channels
Where a stream is for a live trickle of events, a **channel** is for **moving a large amount of data
at once**: a direct streaming pipe between two endpoints, rather than one request and response.
Channels are bidirectional (each end has both a reader and a writer), but here you'll stream in one
direction, uploading a CSV of links. You'll give this its own `bulk-importer` worker so the `link`
worker stays focused on single links.
## Add the worker
Create the importer directory the same way you created `link` in Chapter 1:
```bash
mkdir -p bulk-importer/src
```
Create the worker manifest and package metadata before writing its source:
```yaml bulk-importer/iii.worker.yaml
name: bulk-importer
scripts:
start: pnpm start
```
```json bulk-importer/package.json
{
"name": "bulk-importer",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "tsx watch src/index.ts"
},
"dependencies": {
"iii-sdk": "0.21.4",
"@iii-dev/helpers": "0.21.4",
"tsx": "^4.22.3"
}
}
```
```bash
cd bulk-importer && pnpm install && cd ..
```
## Import a CSV over a channel
The `bulk-importer` worker exposes one function that receives the read end of a channel, streams the
CSV in, and triggers `link::create` from the `link` worker for each row. Create
`bulk-importer/src/index.ts`:
```typescript bulk-importer/src/index.ts
import { registerWorker } from "iii-sdk";
import { Logger } from "@iii-dev/helpers/observability";
const worker = registerWorker(process.env.III_URL ?? "ws://localhost:49134", {
workerName: "bulk-importer",
});
const logger = new Logger();
worker.registerFunction("bulk-importer::import_csv", async (input) => {
const chunks: Buffer[] = [];
for await (const chunk of input.reader.stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const csv = Buffer.concat(chunks).toString("utf-8");
const rows = csv.trim().split("\n").slice(1); // skip the header row
let imported = 0;
for (const row of rows) {
const [code, url] = row.split(",");
if (!url) continue;
await worker.trigger({
function_id: "link::create",
payload: { code: code.trim(), url: url.trim() },
});
imported += 1;
}
logger.info("bulk import complete", { imported });
return { imported };
});
logger.info("bulk-importer ready");
```
Register it with your project:
```bash
iii trigger -n linkly compose::add worker=./bulk-importer
```
## See it work
With the engine running, let's bulk-load some links.
<Note>
Unlike previous chapters this section and Chapter 7 require that you have [node and npm
installed](https://nodejs.org/en/download/current) locally. This is because we're now creating
client side code that runs outside of workers.
</Note>
### Upload a CSV
The uploader is a small standalone script that creates the channel, writes the CSV to the writer
end, and hands the reader end to `bulk-importer::import_csv`. `createChannel` (from
`iii-sdk/helpers`) returns serializable `readerRef`/`writerRef` handles you can pass through a
normal trigger payload. It is not a worker, so give it its own throwaway directory outside your
project:
```bash
mkdir test-channels
cd test-channels
npm init -y
npm pkg set type=module
npm install iii-sdk@0.21.4
```
Save this as `test-channels/import-links.js`:
```javascript import-links.js
import { registerWorker } from "iii-sdk";
import { createChannel } from "iii-sdk/helpers";
const worker = registerWorker(process.env.III_URL ?? "ws://localhost:49134", {
workerName: "uploader",
});
const csv = ["code,url", "mylink,https://iii.dev", "mydocslink,https://iii.dev/docs"].join("\n");
const channel = await createChannel(worker);
channel.writer.stream.write(Buffer.from(csv));
channel.writer.stream.end();
const result = await worker.trigger({
function_id: "bulk-importer::import_csv",
payload: { reader: channel.readerRef },
});
console.log(result);
await worker.shutdown();
```
```bash
node import-links.js
```
```json
{ "imported": 2 }
```
Both new links resolve immediately:
```bash
iii trigger link::resolve code=mylink
iii trigger link::resolve code=mydocslink
```
```json
{
"url": "https://iii.dev"
}
{
"url": "https://iii.dev/docs"
}
```
## Conclusion
Linkly can now ingest a file's worth of links in a single streamed upload through a dedicated
`bulk-importer` worker. Next, in [Ch. 7: Bring in the browser](/tutorials/linkly/frontend), you turn
a browser tab into a worker that creates links and subscribes to the live click stream.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!